Files
ersatztv/docs
timothyandClaude Opus 5 f4473926d4 fix(643): close the status-string dodge, pin the allow-list anchors, fix two doc claims
Round-3 review (MERGEABLE, all findings Low/Nit) — applied anyway, because each is the
over-claim or unpinned-anchor family this PR keeps hitting.

LOW — the `renamed => previous_filename REQUIRED` clause matched `.status` by exact
lowercase string, so any other value took the `else true` branch: a row with
`"Renamed"`, or with no status at all, validated fine and silently dropped its SOURCE
path, letting `git mv ErsatzTV/Program.cs -> docs/a.md` read as docs-only. `.status` is
now checked against a closed set and an unknown value fails closed.

Two things that fix taught me, both caught by my own positive control rather than by
review:
  1. The first predicate was WRONG in a way that gated everything: inside
     `[...] | index(.status)`, jq's `.` is the ARRAY, so `.status` was null and every
     row failed. `$s` is now bound from the row before the context switches. A
     security check that rejects everything looks identical to a working one from the
     failing side — only test_gitea_real_status_values_are_accepted caught it.
  2. The set includes BOTH `changed` and `modified`. Live Gitea 1.25.4 emits `changed`,
     but a closed allow-list built from the wrong vocabulary is worse than the hole it
     closes: it would gate every genuine docs-only PR. The property wanted is "reject
     what we don't recognise", not "enumerate one version exactly".

LOW — three allow-list anchors had no test at all: dropping `^` from the `docs/`
alternative (`ErsatzTV/docs/Program.cs` would exempt), dropping `$` from `.md`
(`x.md.cs` would exempt), and dropping the non-empty-list guard. Since the round-3 `..`
finding WAS an anchor subversion, they are now pinned; all three mutation-verified.

NIT — docs/ci-cd.md called this job "a checkout plus a pure-stdlib pytest run", which
the same file contradicts 450 lines later and which this PR's own record names as the
bug that turned the job red on its first CI run. Also replaced a frozen "111 tests" with
an explicitly indicative figure — the suite is ~190 after rebasing onto main, and a
frozen count has rotted four times this session.

198 passed under both jq 1.8.2 and jq 1.6.

Refs #643, #631

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:24:20 +02:00
..

docs/ — task-signal map

Purpose: route a fresh contributor/agent to the minimal set of docs for the task at hand, instead of a mandatory front-to-back read. Update this doc in the same PR that adds, removes, or retitles a doc below, or that changes which sections a task signal points to.

Start here, always

  • CLAUDE.md (repo root) — project intro: architecture, layout, dev commands, conventions, Task Completion Protocol.
  • docs/contributing.md — established code patterns (CQRS/MediatR, LanguageExt, the ChicoryTV SPA, EF Core dual-provider migrations, FFmpeg pipeline, analyzers, testing). Read before any non-trivial change.

Task signal → minimal sections

Signal Read
Session startup / "what's next" (no issue named) docs/handoffs/chicorytv-issue-queue.md (standing kickoff — two concurrent tracks: orientation ‖ scripts/select-queue.sh 5)
Named-issue pickup Skip queue selection; go straight to focused retrieval — see "Knowledge retrieval" below, then the issue body
Adding/changing a /api/* endpoint docs/api-conventions.md checklist + docs/endpoint-index.md
Adding a ChicoryTV SPA screen docs/spa-conventions.md
Scheduling / playout engine work docs/domain-model.md + decisions catalog rows keyed sched.* (docs/decisions/README.md)
Concurrency / optimistic-locking work docs/api-conventions.md §7a/b/c + docs/decisions/optimistic-concurrency.md
Auth / security-surface work docs/decisions/api-auth-security.md
CI / release pipeline work docs/ci-cd.md + docs/decisions/release-ci-governance.md
Live local run / Playwright-MCP verification docs/e2e-local.md + scripts/e2e-local.sh
Adding/changing a UI-E2E browser flow docs/e2e-local.md → "UI-E2E harness" + scripts/e2e-ui.sh
What does a test suite cover docs/testing.md
Legacy Blazor route lookup docs/blazor-route-parity.md (historical #91 phase (b) inventory)
"Why do we do X this way" / challenging a convention Catalog-first: docs/decisions/README.md (active rows) → follow the row's link to docs/decisions/records/<area>/<topic>.md for full rationale. docs/decisions/archive/<area>/ only for "what did the rule used to be."

Knowledge retrieval (MemPalace + catalog + Gitea)

These four rules are the seam agreed with server-management#642 (the Gitea→MemPalace exporter). They apply whether the question comes up via MemPalace, a grep, or a stale comment:

  1. Current conventions/decisions → catalog-first. Start at docs/decisions/README.md; discover via the ErsatzTV-Decisions wing (active) / ErsatzTV-Decisions-Archive (superseded/retired). Resolve by topic/key, never by chasing a file path.
  2. Issue history → evidence, not authority. The Gitea-ErsatzTV wing is historical narrative that may be stale; it never overrides current Markdown.
  3. The breadcrumb rule (the crux behavior change). A file path named inside a historical issue comment (e.g. "grep docs/decisions.md 2026-07-17", "see …") is a breadcrumb, not a live pointer. Find the current rule via the catalog / active wing by concept; do not treat the named path as current. (Why it's safe: still-current → in the active wing, breadcrumb resolves; superseded → the active wing returns the successor and a literal follow lands on a record that announces its own status: superseded; retired → the active wing returns nothing, which is itself the signal. The validator-enforced move-to-archive/ is what prevents the catastrophic "superseded rule read as current" case.)
  4. Fallback when MemPalace is stale/down: docs/decisions/README.md catalog, then rg '^`key: <dotted.key>`' docs/decisions/. MemPalace is never authority nor sole fallback.

MemPalace is candidate discovery only — every passage is verified against its cited Markdown/Gitea source before use. Never derive live queue state from MemPalace, #237, or historical comments; queue state is live Gitea state, retrieved via scripts/select-queue.sh (see docs/handoffs/chicorytv-issue-queue.md). Full retrieval contract (altitude/precedence, staleness bounds, what's mined per issue): docs/handoffs/chicorytv-issue-queue.md → "Knowledge retrieval".

Also present in docs/

  • docs/domain-model.md — what the app IS: entity glossary, channel→playout→schedule/block concept map, where each concept is edited in the SPA.
  • docs/api-conventions.md — checklist for adding/changing a /api/* endpoint (controllers, DTOs, error mapping, auth, OpenAPI regen, tests).
  • docs/spa-conventions.md — playbook for adding a screen to the ChicoryTV React SPA.
  • docs/e2e-local.md (+ scripts/e2e-local.sh) — how to run a live local instance for manual or Playwright-MCP verification.
  • docs/testing.md — testing map: what each *.Tests project / web suite covers, golden-file nets, the timezone-independence rule, how to run subsets, the per-PR verification gate.
  • docs/blazor-route-parity.md — historical record of the completed #91 phase (b) cutover: the Blazor Server UI is removed and every legacy route now 302-redirects to its SPA equivalent (or falls through to the catch-all → /app). Read it for the full legacy→SPA route inventory.
  • docs/decisions/records/<area>/<topic>.md — one active decision record per file, YAML frontmatter (key/title/status/since/supersedes/superseded-by, plus optional stale-after/sources — ersatztv#603), rationale prose in the body. The filename is the key, so one-active-record-per-key is a filesystem property (ersatztv#610). docs/decisions.md and the topic files remain as the lifecycle-schema narrative plus a "Records formerly in this file" index, which is what keeps older date-based pointers resolvable. Generated active view: docs/decisions/README.md (catalog / task router) — start there. Superseded/retired records live in docs/decisions/archive/ and are read only for history, never for "what is the current rule."
  • docs/ci-cd.md — build/test/release pipeline, versioning, dependency management.
  • docs/rest-api.md — REST API design doc for ersatztv#2 (goals, conventions, per-slice plan). Largely superseded day-to-day by docs/api-conventions.md; read this for the original rationale.
  • docs/mcp.md — the ErsatzTV.Mcp stdio JSON-RPC MCP server (#58): how it wraps /api/v1 as read + cautious-write tools, its config/env vars, auth, security posture, and the tool catalog.
  • docs/channels.md — Channel entity field reference.
  • docs/m3u-xmltv.md — M3U/XMLTV generation overview (ChannelPlaylist, GetChannelGuideHandler).
  • docs/fork-strategy.md — divergence policy vs upstream ErsatzTV.
  • docs/design-sync.md — Claude Design ↔ repo screen workflow (#92).
  • docs/endpoint-index.md — generated REST endpoint index (method/path/operationId/summary per OpenAPI tag). Do not edit by hand; regenerated by scripts/generate-endpoint-index.py / scripts/update-openapi.sh.
  • docs/handoffs/chicorytv-issue-queue.md — static session kickoff prompt + workflow lore. Queue state is live Gitea state, retrieved each session via scripts/select-queue.sh — see that file's standing kickoff for the two concurrent tracks (orientation ‖ selection). ersatztv#237 is a closed, archival historical tracker (superseded by startup.parallel-orientation in docs/decisions.md) — not a live pointer.
  • docs/tracker-retrofit-triage-237.md — audit trail for the #524 triage of ersatztv#237's 111 comments (method, per-comment classification, totals). Evidence for the docs.tracker-comment-retrofit decision; read it only when triaging another over-cap tracker.
  • docs/handoffs/rest-api.md — original handoff prompt for kicking off the REST API work (#2).