Files
ersatztv/docs/README.md
T
timothyandClaude Opus 5 7db4101b24
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
PR Gates / Docs update reminder (pull_request) Successful in 10s
PR Gates / decisions lifecycle (pull_request) Successful in 24s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 25s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 21s
review-verdict/h10 Awaiting review verdict for 7db4101
Review verdict / Set review-verdict status (pull_request_target) Successful in 28s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m56s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m14s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
fix(778): the escape emitted TWO backslashes, turning a false-open into a false DENY
Seventh cold review. One High, one Medium, three Low, seven Nit — all in the two
newest commits, which is where every round of this PR has found its defects.

HIGH, and it is my own fix from the previous commit. In jq source `"\\\\"` decodes to
TWO backslashes, so escaping produced `\\.` — "a literal backslash, then any
character" — instead of an escaped dot. Every rule name containing a metacharacter
became UNMATCHABLE, and a rule named `a[b` crashed jq outright (swallowed by
`|| true`). Verified: `release/26.*` no longer matched base `release/26.4`, so the
fallback found nothing and hard-DENIED with the stated cause "has NO branch protection
at all" — converting a false-open into a false deny, which the block's own comment
calls the worse outcome. One character: `"\\" + .c`. Correct across 14 rule/base pairs.

WHY MY TEST MISSED IT, which is the transferable part: it asserted only the NEGATIVE
direction (`mai.` must not match `main`). A rule matched literally and a rule made
unmatchable both fail to match the wrong base, so the assertion passed for the wrong
reason. Only a rule that SHOULD match separates them, and there was no positive
control. There is now — plus a char-class case — and both go red against the
over-escaped version. That also needed a base containing a dot: a rule cannot carry a
metacharacter and still match `main`, so the first attempt at the positive control was
unsatisfiable by construction.

MEDIUM — four live claims that the population "derives from the filesystem", left
standing by the commit that replaced that mechanism: the guard's own docstring 45
lines above a comment shouting the opposite, the inventory heading 21 lines under
"Every git-tracked file", the docs/README entry, and — worst — the record's
`mechanics:` frontmatter, which is the copy the catalog and MemPalace mirror, so
discovery would have returned the superseded lesson. All corrected.

LOW/NIT: the URL-encoding test grepped the source for `@uri` (it now asserts the URL
actually requested, and reddens when the encoding is removed); the hoist comment said
"every path below" without noting the docs-only enumeration above it (bounded — that
path is a passthrough to a human prompt, never a grant); a now-unreachable guard is
annotated rather than left reading as live; `issue-qualification-audit.sh` was `N/A`
while `select-queue.sh` was `UNSAFE-KNOWN` on the same argument, and
`security-scan.sh` claimed "one step" for a pull-then-run over a mutable tag — both
regraded; the `PINNED` definition now says what separates its second shape from an
`N/A` "one step" row (the identifier's immutability, not the step count); the section
parser raises a message naming both required headings instead of a bare ValueError;
and the record's body is rewrapped.

Decisions-Edit: yes

refs #778

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 18:21:04 +02:00

136 lines
11 KiB
Markdown

# 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` |
| Proposing a new guard / CI check / regression test convention | `docs/defect-shapes-773.md` §4 (detector menu + the classes where no detector is plausible), then the two rules every guard must satisfy: `docs/decisions/records/testing/guard-derives-population-from-source.md` and `…/guard-ships-with-mutation-proof.md` |
| Adding / changing / deleting a guard file | `docs/guard-inventory.md` — every guard's row is machine-checked by `scripts/tests/test_guard_inventory.py`, so a new guard must acquire a row before the suite goes green |
| Writing code that reads live Gitea/remote state and then acts on it | `docs/decisions/records/process/check-and-use-pins-a-version.md`, then `docs/remote-state-inventory.md` — a new executable under `scripts/` (**excluding `scripts/tests/`**), `.claude/hooks/`, `.husky/` or `.gitea/workflows/` must acquire a row there before `scripts/tests/test_remote_state_inventory.py` goes green |
| Finding every site that references a symbol (multi-site fix/sweep) | `docs/local-lsp-tooling.md` — which surface answers, and why a delegated agent must be pointed at the `csharp-lsp` MCP tools rather than the `LSP` tool |
| 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/local-lsp-tooling.md`** — the code-intelligence surfaces (the `LSP` tool's three servers and
the `csharp-lsp` MCP server): how each is configured, which one a **subagent** can actually reach,
the traps (a cold server answers the first query with a confidently partial result), and
`scripts/check-local-lsp.sh` to verify the preconditions. Read before briefing an agent to find
every site referencing a symbol.
- **`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/defect-shapes-773.md`** — root-cause analysis of the recurring defect shapes across the
whole closed-issue corpus (#773): the measured class ranking, the four families they consolidate
into, the cheapest mechanical detector per class, the classes where **no** detector is plausible,
and an audit of which configured hooks/MCP servers/LSPs are actually invoked. Read it before
proposing a new guard or CI check — §4 is the detector menu, and it argues against enumerating
cases one incident at a time.
- **`docs/remote-state-inventory.md`** — every executable in `scripts/` (**excluding
`scripts/tests/`**), `.claude/hooks/`, `.husky/` and `.gitea/workflows/` that reads live remote
state and acts on that read, classified `PINNED` / `CAS` / `UNSAFE-KNOWN` / `N/A` with the window
and what bounds it. Code outside those directories — C#/TypeScript guards, `web/`, and the test
suites themselves — is out of scope, and the doc states that rather than implying coverage.
The population is derived from `git ls-files` and compared for set equality by
`scripts/tests/test_remote_state_inventory.py`, so a new script that talks to a remote service
cannot ship unclassified. Read it with `process.check-and-use-pins-a-version`; it is that record's
detector, since the class has no plausible linter (`docs/defect-shapes-773.md` §4 detector D).
- **`docs/guard-inventory.md`** — every executable guard file, what it blocks, whether it is a
`GUARD` or `TOOLING`, and whether it ships a mutation proof (`MUTATION` / `BEHAVIOUR-ONLY` /
`NONE`) with a `file::function` ref. The population is derived from the filesystem and the
workflow/hook call sites and compared for set equality by `scripts/tests/test_guard_inventory.py`,
so a new guard cannot ship unclassified and a renamed test cannot leave a row claiming coverage it
has lost. Guards implemented inline in workflow YAML are deliberately outside that population —
the doc states the limit rather than implying coverage.
- **`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).