Round three, reviewed twice in parallel (Codex gpt-5.6 and a cold Claude brief). Both converged
independently on the same two MEDIUM findings, which are the branch's own defect class reproduced
by its most recent commits: a test asserting a guarantee it does not provide.
1. THE PROOFS DID NOT EXERCISE THE CODE THEY NAMED. `test_MUTATION_a_DUPLICATE_row_is_reported`
rebuilt the comparison with its own `len(set(...))`, and `test_MUTATION_a_SHADOWING_env_key_is_
reported` built a synthetic dict and intersected it by hand. Both stayed GREEN with the live
check disarmed — a behavioural test wearing a MUTATION name, which is exactly what
`testing.guard-ships-with-mutation-proof` excludes.
`duplicate_row_faults(rows)` and `env_shadow_faults(docs)` are now named detectors; the live
test and its proof both call them. Verified by reproducing the reviewers' own demonstrations
against the fix:
duplicate detector neutered -> REDDENED "the DETECTOR did not report a second row for ..."
shadow detector neutered -> REDDENED "the DETECTOR did not report a shadowing job-level key"
2. `test_a_commented_out_marker_does_NOT_count` CLAIMED TO GUARD A TEXT-SCAN REGRESSION AND DID
NOT. It re-implemented the read inline and never called `declared_roles`. One reviewer built a
comment-blind grep reader reproducing all 17 current values byte-identically and ran every
non-parametrised test in the file: nothing went red.
`declared_roles(paths=None)` now takes an injectable population, so the test drives the REAL
reader over a fixture, asserts the fault function reports it, and pins that a live marker in the
same shape is still read (so the assertion cannot be satisfied by a reader returning None for
everything). Verified: installing the grep reader now reddens it.
RENAMED `advisory` -> `report-only`. docs/ci-cd.md already calls `functional-e2e` an advisory job
in a DIFFERENT sense — not a required context, though it can certainly fail — while this branch
declares that job `guard`. A reader would infer the wrong marker for exactly the job whose marker
is `guard`. The value is now unambiguous, its Kind is `REPORT-ONLY`, and the manifest's declared
`expect` string moved in lockstep with the message it matches.
The rename also caught its own adjacent defect: `REPORT-ONLY` contains a hyphen, which the row
regex's `[A-Z]+` Kind group did not match, so the docs-reminder row stopped parsing. That surfaced
as a loud red naming the job — the fail-closed property documented one commit earlier, working.
Widened to `[A-Z-]+`, matching the `BEHAVIOUR-ONLY` convention in the sibling table.
Nine further accuracy findings, each measured before fixing:
* "would therefore flag every job" — false. git/jq/python3/curl flag 4 of 8. And the REASON was
wrong too: `small` is git-only and provisions Python via actions/setup-python, while `build`
runs on stock ubuntu-latest, so "the bare runner has them" conflated two lanes. They are
excluded because they do not DISCRIMINATE.
* "four lines from the container: block" / "in one diff hunk" — false. Measured 6, 58, 6, 7, 7;
`migrations` has the whole `services:` block in between. The property the guard rests on is
INDEPENDENCE, not proximity, and the docs now say so.
* "the merge semantics never have to be relied upon" — too strong. The check proves no NAME
COLLISION; it does not prove the runner merges. That evidence is in-repo and live and is now
cited: `build`'s Smoke step declares its own step-level `env:` and consumes workflow-level
`${IMAGE}` on the same line, on every push to main.
* "every malformed row fails closed" — over-broad. A malformed DUPLICATE is invisible: its job is
already satisfied by the well-formed row. Stated.
* The `scan` Blocks cell said "a marked job's run: body"; the ban's scope is DELIMITER_BAN_JOBS
(the required contexts PLUS `build`), which is wider, and `scan` carries no `if:` so it runs on
PRs too.
* The functional-e2e row credited `e2e-local.sh` (TOOLING, only boots the instance) for curl
assertions that live in `e2e-functional.sh` (GUARD).
* `report-only` was defined nowhere; now defined as a verdict the job cannot enforce, with the
limit stated (nothing detects a `guard` job whose checks are all continue-on-error).
* "inline rows carry Proof: NONE" was stated universally while two rows cite a pytest; hedged,
and each such row names which PART it covers.
* "Exactly three jobs are `none`" was an unchecked prose enumeration in three sites, in a branch
that deleted one for that reason. Removed; the markers are the source.
* Plus: the ci-cd tool list was short by `dotnet-ef` and `playwright`; docs/README said only a
`guard` job needs a row; "all three checks fail together" read as the opposite of "go blind";
and #774's unrealised consequence is now subjunctive rather than past tense.
Suite: 1178 passed, 2 skipped, 1 known flake (test_hook_fire_log's production-log guard, which
compares mtimes under the SHARED ~/.cache/ersatztv/hook-fire/ — four session logs were written
during the window; 78 passed in isolation. ersatztv#809/#822, evidence posted there).
Refs #786
Refs #789
148 lines
15 KiB
Markdown
148 lines
15 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` |
|
|
| Explaining a consequential settings field in the SPA (summary → hover/tap panel → docs link) | `docs/spa-conventions.md` §15 — use the shared `FieldHelp` trigger and put the copy in the screen's own `FIELD_HELP` record; the icon, the gesture and the a11y contract are fixed |
|
|
| Graphics element / overlay work (text bug, On Now / Next, watermark-vs-`[vge]`) | `docs/graphics-elements.md`, then decisions catalog rows keyed `graphics.*` |
|
|
| Scheduling / playout engine work | `docs/domain-model.md` + decisions catalog rows keyed `sched.*` (`docs/decisions/README.md`) |
|
|
| Adding or changing a paged list handler (a page plus a `TotalCount`) | Resolve `api.paged-count-matches-page-query` via `docs/decisions/README.md` — for an EF-backed filtered list, count the SAME query you page, with includes appended to the page chain only; where the count and the page are separate methods, a test pins their agreement. Then `api.paging-zero-based` for the `pageNum`/`pageSize` contract |
|
|
| 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 rules every guard must satisfy: `docs/decisions/records/testing/guard-derives-population-from-source.md`, `…/guard-ships-with-mutation-proof.md` and `…/mutation-claims-are-executed.md` (a `MUTATION` grade carries a DECLARED clause mutation that is re-run every suite) — plus `…/verification-code-needs-its-own-proof.md`, which extends the same obligation BEYOND guards to the harness, wrapper or checker doing the checking, and says where its proof lives when the checker holds no row |
|
|
| Adding or bounding a consequential numeric config field (an FFmpeg profile tunable, a pipeline knob) | `docs/api-conventions.md` §3d — reject out of range with a 422 naming the bound and its consequence, never accept-then-rewrite; validate against the constants the renderer reads, keep the render-time clamp for pre-existing rows, and let an UNCHANGED legacy value through on update. Then `api.ffmpeg-profile-numeric-bounds` |
|
|
| Testing a surface gated by config / an env var / a credential | `docs/decisions/records/testing/deny-path-at-production-config-value.md` — cover the setting absent, at its production value, and each opt-out, and assert the DENY branch |
|
|
| Touching a full-replace write path or a hand-built request object | `docs/decisions/records/testing/full-replace-asserts-field-list.md` — derive the field list from the DTO and assert set equality; reconcile by id where child state exists. In the SPA the same rule is enforced by the type system: `docs/spa-conventions.md` §4b — build the body as `Complete<T>`, annotating BOTH the wrapper parameter and every construction site |
|
|
| Writing or editing any doc, or answering a review finding in prose | `docs/decisions/records/docs/no-session-narrative.md` — the doc records the END STATE; the path to it goes in the commit message. Apply the who-benefits test, and read the carve-out before you cut (dated measurements, stated snapshot boundaries and tested-and-rejected results stay) |
|
|
| Adding, renaming or removing a workflow JOB | `docs/ci-cd.md` → "Per-job declarations" — every job declares `env.CI_JOB_ROLE` (and, in `docker-build.yml`, `env.CI_EXECUTION_CLASS`); a missing or unknown value fails `scripts/tests/test_workflow_job_guards.py` / `…/test_ci_image_pin_population.py`, and a `guard` **or `report-only`** job also needs a row in `docs/guard-inventory.md` → "Workflow-job guards". Rationale: `docs/decisions/records/testing/workflow-declares-its-own-job-metadata.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, and a row graded `MUTATION` must also acquire a declared clause in `scripts/tests/mutation_manifest.py` |
|
|
| 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 of the three surfaces answers, and why a delegated agent must be pointed at an MCP server (`csharp-lsp`, or `serena` after an `activate_project`) rather than the `LSP` tool, which no subagent has been observed to reach |
|
|
| 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,
|
|
the `csharp-lsp` MCP server, and `serena`): how each is configured, which ones a **subagent** can
|
|
actually reach, the traps (a cold server answers the first query with a confidently partial result;
|
|
serena needs an `activate_project` per directory), 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/graphics-elements.md`** — graphics element (overlay) schema reference: how elements are
|
|
discovered and attached, the YAML parsing traps (an unknown key disables the element outright), the
|
|
full text-element field table including the #732 background box, and why `[vge]` in a filter graph
|
|
does not imply a graphics element is bound.
|
|
- **`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).
|