Files
ersatztv/docs
timothyandClaude Opus 5 ea888011aa
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 16s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 16s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Awaiting review verdict for ea88801
Review verdict / Set review-verdict status (pull_request_target) Successful in 18s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 7m4s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m55s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 4m22s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Canceled after 0s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Canceled after 0s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Canceled after 0s
fix(823): the decision record argued BOTH readings — and the per-dimension mutant that survived the fixture
Round-three review findings. One HIGH, and it was in the durable artifact
rather than the code.

THE HIGH: the record stated the shipped reading and its inverse.

The semantic reversal (empty -> unrestricted) rewrote the residual and the
write-half of `media.nullable-primitive-collection-mutation` but left the
ORIGINAL reasoning standing two sentences earlier: "A null reads as EMPTY, so
the item matches nothing"; "the REJECTED alternative was the All*() set";
"SKIPPING the row is the conservative repair". The shipped code is
`?? AllDaysOfWeek()` -- precisely the alternative that passage calls rejected.
The previous commit then inserted residual (1), which reasons entirely FROM
the All*() reading, two sentences after the sentence denying it.

That is worse than a stale comment. A session resolving this key -- or reading
the MemPalace mirror, which carries `rule:` verbatim -- would have been told to
write the guard the other way, i.e. talked into the `[]` reading that the same
record elsewhere argues is data corruption one save later. Replaced the whole
passage, then swept the record for every other mention of the empty reading
rather than trusting the one replacement: the only survivor is the new sentence
that records EMPTY as the rejected alternative, which is the direction that
stops it being re-adopted.

THE MEDIUM: one arrangement did not close the hole it claimed to.

The discriminating control added last commit nulls DaysOfWeek against a
restrictive MonthsOfYear. It excludes "any NULL matches unconditionally" only
for that dimension. The review supplied the surviving mutant --
`if (item.MonthsOfYear is null) { return item; }` ahead of the checks -- and
traced it green through all nine tests. Verified by EXECUTION, not by reading:
applied to the previous fixture it passes; applied now it FAILS 1 of 11. Each
of the three dimensions is now nulled against a restriction on a different
dimension.

The rest, all from the same round:

- The coverage guard's test detection listed attribute TYPES, and each list
  falsely reddened whatever it omitted: TestAttribute alone missed [TestCase],
  and the three-type replacement missed [Theory]. Now decided by NUnit's own
  ITestBuilder/ISimpleTestBuilder interfaces, which cannot fall behind the
  vocabulary. It also dropped BindingFlags.Static (GetMethods() defaults to
  including it), which would have falsely reddened a static test method.
- The same guard accepted an ABSTRACT fixture -- NUnit never instantiates one.
  The indexer population already filtered IsAbstract; the fixture side now
  mirrors it.
- The record's `mechanics:` still described the old `[Test]`-only clause, in
  the same file the change edited.
- An <inheritdoc> made the ProgramScheduleAlternate empty-case test inherit a
  docstring written from the PlayoutTemplate test's viewpoint.

Two more mutations executed:
- `if (item.MonthsOfYear is null) return item;` -> 1 red, 10 green. This is the
  mutant that survived the previous head; it no longer does.
- an abstract type named in the covered set -> coverage guard red.

Local gate: ErsatzTV.Tests 2091 passed / 6 skipped (the three fixtures' MySQL
halves, skipping visibly without ETV_TEST_MYSQL_CONNECTION), Core.Tests 697/1
-- 0 failures. Format clean, no BOM on the touched set. decisions_validate OK.

Refs #823
Refs #824

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019zUmJZHhVP7kXg5DV237TW
2026-08-29 20:43:07 +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
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
Writing a test whose behaviour is PROVIDER-SPECIFIC (collation, a value converter, data-migration DML) docs/testing.md → "Provider-parity fixtures (opt-in MySQL)" — run one fixture body against both providers via ETV_TEST_MYSQL_CONNECTION; without it the MySQL arm Assert.Ignores visibly, and CI does not currently run it (ersatztv#627)
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 GIT INDEX (not a filesystem walk, since ersatztv#806) 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).