Retire the line-level append-only mechanic (ersatztv#303 H9) in favor of the lifecycle validator
built in Tasks 1-5. .claude/hooks/decisions-guard.sh is now a thin fail-open shim around
scripts/decisions_validate.py; .husky/pre-commit calls it for the structural (working-tree) checks,
.husky/commit-msg drops the old staged/[decisions-edit]-deletion block and keeps only the
Co-Authored-By check. The Gitea decisions-guard job is renamed "decisions lifecycle" and now runs
decisions_validate.py --base/--head (structural + body-diff + no-vanish) and
build_decisions_catalog.py --check (active catalog drift), with actions/setup-python@v5 added since
the bare `small` lane doesn't guarantee python3; the old 1800-line consolidation-floor step is
removed (replaced by the validator's aggregate active-corpus budget). docs/decisions.md's header is
rewritten from append-only to lifecycle framing (metadata schema, statuses, generated catalog,
archive, same-PR supersession); [decisions-edit] is re-scoped (not removed) to rationale-prose
edits/factual corrections only. docs/ci-cd.md's release ritual and hook/job descriptions are
rewritten to match.
Also fixes a pre-existing validator false-positive surfaced while sanity-checking against
origin/main: Task 6's #303 H9/H3 split (commit d09be57e) renamed the archived record's heading
away from the pre-split original, which the validator's heading-based relocation check reads as
"removed without an archive copy." Restored the archived heading to match the original text
(functionally unchanged — still status: superseded, same key) and updated the two prose
cross-references (migration-map.md, release-ci-governance.md) that pointed at the old anchor.
PR1 scope only (per brief): does NOT wire the kickoff-guard CI step or touch the
kickoff/README/select-queue docs — that's Task 8/PR2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The archived docs.append-only-guard record bundled the genuinely-superseded
H9 (decisions.md append-only) with the independent, still-active H3
(root-screenshot pre-commit guard), relocating both into archive/ and
burying an active decision in the history wing. Split into two records:
docs.append-only-guard stays in archive/release-ci-governance.md,
status: superseded, H3 content removed and replaced with a forward
pointer; ci.root-screenshot-guard is a new status: active record in the
active docs/decisions/release-ci-governance.md, with the H3 rationale
paragraph lifted byte-identical from the original bundle. Updated
migration-map.md (new row + a "Migration conventions" note: never
relocate an active decision to archive/) and regenerated
docs/decisions/README.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate #406 (ci.runner-placement) and #412 (ci.peak-anon-measurement) to the lifecycle
schema as exemplars; #390 and #411 confirmed prose-only (no standalone record). Dogfood the
append-only->lifecycle supersession: docs.append-only-guard moved to archive/, superseded by
new active docs.decision-lifecycle. Stand up docs/decisions/archive/ + migration-map.md.
Refs #521
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_anchor() mapped each space/hyphen to its own '-' without collapsing runs,
so the standard heading separator " — " (space, em-dash, space) produced a
double hyphen in every generated anchor. Since nearly every real decision
record heading uses that separator, this made the catalog emit a dead link
for essentially every row. Fix: after building the char list, collapse
consecutive '-' into one and strip leading/trailing '-' via re.sub, matching
how Goldmark/GitHub/Gitea sluggers behave.
Also fixed main() writing an extra trailing newline (want already ends in
"\n", then "+ \n" appended a second one) so docs/decisions/README.md now
ends with exactly one trailing newline; --check still compares via .strip().
Added test_anchor_collapses_em_dash_and_keeps_underscore to pin the anchor
behavior against the reported iptv.base_url case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds scripts/build_decisions_catalog.py, which renders docs/decisions/README.md
as a compact table of only 'active' decision records (sorted by key), and its
test scripts/tests/test_build_catalog.py. Supports --check for CI drift
detection. No decision records are migrated yet, so the generated catalog is
currently empty (banner + header only) — expected at this stage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two concurrent adds of the same item both membership-check it absent, both
insert the CollectionItem composite key, and the loser's
SaveChangesForcingVersion threw an uncaught DbUpdateException (SQLite 19 /
MySQL 1062) -> 500. Now the loser is an idempotent no-op.
- ConcurrencyExtensions.TrySaveChangesForcingVersion: bool-returning sibling
that catches only a classified unique/PK violation and returns false.
- 10 single-item Add*ToCollection handlers: return Unit.Default (no-op, skip
fan-out) on false — the racing winner already inserted + rotated + rebuilt.
- Bulk AddItemsToCollection: retry on a fresh context against recomputed
membership so a partial-overlap collision doesn't drop the non-colliding
items (bounded loop; common no-collision path runs once).
- Provider detection via a TvContext.IsUniqueConstraintViolation static
delegate (matches the existing IsSqlite/LastInsertedRowId provider seam),
wired from Startup to SqliteErrorClassifier / MySqlErrorClassifier.
- Add*ToPlaylist is NOT affected (PlaylistItem has its own identity PK; a
playlist may legitimately contain the same item more than once).
Tests: a negative-control anchor proves the race genuinely throws a classified
exception; end-to-end handler tests reproduce a real cross-connection race via
a shared-cache SQLite harness + a SavingChanges interceptor (the single-conn
in-memory fixture cannot). Every fix-dependent test verified to fail with the
catch disabled.
Docs: api-conventions.md §7a (idempotent insert under concurrency) +
decisions/optimistic-concurrency.md.
fixes#308
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate docs/decisions.md (1923 -> 1028) by extracting four cohesive
topic clusters into docs/decisions/ (optimistic-concurrency,
api-auth-security, release-ci-governance, spa-modularization) — content
relocated verbatim (lossless; all rationale + reversals preserved), main
Index rebuilt to reference the topic files plus the remaining in-file
entries, docs/README.md points back at the decisions Index. Add the
v26.9.0 row to the ci-cd.md version table.
refs #340
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>