Files
ersatztv/docs/decisions/retrieval-eval.md
T
timothyandClaude Opus 4.8 fb6720ea27 fix(521): de-dup 6 overlapped records; guard duplicate metadata blocks; exclude retrieval-eval; complete eval bank [decisions-edit]
- Exclude docs/decisions/retrieval-eval.md from active decision parsing
  (_NON_DECISION_FILES); its `## N.` eval-question headings were being
  miscounted as 7 legacy-unmigrated records.
- Add decisions_lib.metadata_line_count() + a decisions_validate guard
  that fails a record with more than one `key:` metadata line, so a
  stacked-metadata-block migration bug (which the parser silently
  tolerated by reading only the first block) can't recur unnoticed.
  TDD: test_duplicate_metadata_block_fails / test_single_metadata_block_passes.
- De-duplicate the 6 docs/decisions.md records left with two stacked
  metadata blocks (scan.getoraddfolder-db-lookup #488,
  scan.musicvideo-reconciliation #494, scan.jellyfin-mixed-content-library
  #489, iptv.logo-drives-bug-preset #67, ffmpeg.qsv-decode-encode-split
  #498, ci.small-lane-git-only server-management#639), merging the union
  of Signals/paths/issues/Mechanics from both blocks and keeping the
  richer Rule wording; rationale prose untouched.
- Fill in the deferred Q6b row in docs/decisions/retrieval-eval.md now
  that startup.parallel-orientation is active in docs/decisions.md,
  scoring it as a real active-vs-superseded question against the
  archived docs.queue-state-gitea-tracker.
- Regenerate docs/decisions/README.md via build_decisions_catalog.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:54:52 +02:00

193 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Retrieval-eval question bank (decision-lifecycle corpus, #521)
This bank measures **correct active-record selection + citation**, not mere semantic proximity.
The migrated decision corpus (`docs/decisions.md` + `docs/decisions/*.md`, catalog at
`docs/decisions/README.md`, archive at `docs/decisions/archive/`) intentionally keeps a superseded
or retired record's prose *verbatim* next to a live `superseded-by`/`supersedes` pointer. A retrieval
approach that matches on wording alone can walk straight past that pointer into the archived record
and return a decision this project no longer follows. For each question below, the only correct
answer is the cited active `key` (status: `active`, in a file `active_files()` globs) — landing on
an archived/superseded key, or missing a key that already answers the question (and thus proposing a
reimplementation), is scored a **miss**, regardless of how relevant the returned text reads.
This bank is run as the **Task 10 cold-agent retrieval sim**: a fresh agent is given the catalog
(`docs/decisions/README.md`) plus the active `docs/decisions/*.md` wing and asked each question with
no other context, then graded on whether it names the expected `key` and cites the right file.
Verification: every `key` cited below was confirmed present and `status: active` via
`PYTHONPATH=. python3 -c "import scripts.decisions_lib as dl; [print(r.key, r.status) for f in dl.active_files() for r in dl.parse_file(f) if r.key]"`
against this worktree's corpus on 2026-07-21.
---
## 1. Paraphrased task → decision discovery
**Q1.** "I'm adding a new REST endpoint for a feature that touches three tables — do I need to stand
up a service/business-logic layer, or can the controller call MediatR directly?"
- **Expected key:** `api.mediatr-passthrough`
- **File:** `docs/decisions.md`
- **Why:** The rule is literally "thin controllers over existing MediatR handlers, no new
service/business-logic layer" — answer is NO, don't add one. A naive search might instead surface
`mcp.server-foundation` (also MediatR-adjacent, wrong layer) or nothing at all if it only matches
on "REST endpoint" rather than the layering question.
**Q2.** "Should the playout-build-finished notification go out over a websocket/SignalR push channel
so the SPA doesn't have to poll?"
- **Expected key:** `api.async-op-contract`
- **File:** `docs/decisions.md`
- **Why:** The active rule explicitly rejects a live push channel in favor of an `isLocked`
HTTP-observable flag on list/detail GETs as the substitute. A naive search keying on "poll" alone
might return `spa.playback-troubleshoot-poll` (a different, narrower polling decision for the
troubleshoot screen) instead of the general async-op contract that actually governs new
queue-triggering endpoints.
## 2. Exact code/path lookup
**Q3.** "Where do new API response DTOs live, and what nullable pragma convention do they follow?"
- **Expected key:** `api.response-dtos`
- **File:** `docs/decisions.md`
- **Why:** Direct hit — rule gives the exact path pattern
(`ErsatzTV.Core/Api/<Domain>/*ResponseModel.cs`) and the file-scoped `#nullable enable` convention.
No plausible superseded alternative exists for this key.
**Q4.** "Where does the `EntityLocker` implementation live, and how does it avoid a torn
check-then-set race on its lock flags?"
- **Expected key:** `locking.entitylocker-atomic-flags`
- **File:** `docs/decisions.md`
- **Why:** Rule + Mechanics cite `ErsatzTV.Infrastructure/Locking/EntityLocker.cs` and the
`Interlocked.CompareExchange`-guarded atomic-flag fix directly; no other record touches this file.
## 3. Active-vs-superseded (selecting the archived record here is a FAILURE)
**Q5** *(verbatim, coordinated with server-management#642).* "Is #390's small-lane CI move current?"
- **Expected answer:** **No** — superseded by `ci.runner-placement`.
- **Expected key:** `ci.runner-placement`
- **File:** `docs/decisions.md`
- **Why:** #390 moved `docker build` jobs onto the `small` runner lane to dodge queue time; #406
reversed that (worst-case memory, not runtime, was the real constraint) and the durable rule now
lives under `ci.runner-placement` (and its sibling `ci.small-lane-git-only`, which defines
`small` by job *kind*, not usual runtime). #390 itself was never given its own `##` record (it's
prose-only, referenced inside `ci.runner-placement`'s own Signals line) — answering as if #390's
move still holds, or citing #390 as a standalone current decision, is the failure mode this
question targets.
**Q6.** "Is `docs/decisions.md` still kept append-only by a line-deletion-diff CI/hook guard?"
- **Expected answer:** **No** — that mechanism is superseded.
- **Expected key:** `docs.decision-lifecycle`
- **File:** `docs/decisions.md`
- **Superseded record a naive search might return instead:** `docs.append-only-guard`
(`docs/decisions/archive/release-ci-governance.md`, `status: superseded`,
`superseded-by: docs.decision-lifecycle@2026-07-21`)
- **Why:** `docs.append-only-guard`'s prose (numstat-deleted-count hook + CI job) still reads as a
perfectly good, on-topic answer to "is decisions.md append-only" — that's exactly the trap: the
*mechanism* it describes was replaced by the lifecycle-schema validator
(`scripts/decisions_validate.py`) that this migration introduced. Selecting the archived record
instead of following its `superseded-by` pointer to `docs.decision-lifecycle` is the scored
failure.
**Q6b.** "What is the current source of live queue state at session start?"
- **Expected answer:** Two concurrent tracks — Orientation (docs/README.md task-signal map → the
active decisions catalog) and, only when no issue is named, Selection
(`scripts/select-queue.sh N`, a deterministic query over live Gitea state). Selecting the archived
`docs.queue-state-gitea-tracker` ("queue state lives in pinned tracker issue #237") is a **FAILURE**
#237 closed 2026-07-13 and is now a single archival breadcrumb that MUST NOT be read for live
state.
- **Expected key:** `startup.parallel-orientation`
- **File:** `docs/decisions.md`
- **Superseded record a naive search might return instead:** `docs.queue-state-gitea-tracker`
(`docs/decisions/archive/startup.md`, `status: superseded`,
`superseded-by: startup.parallel-orientation@2026-07-21`)
- **Why:** `docs.queue-state-gitea-tracker`'s prose ("Pinned tracker issue #237 holds the goal +
ordered arc") still reads as a plausible, on-topic answer — that's the trap: the arc completed and
#237 closed, so `scripts/select-queue.sh` (2026-07-19) replaced the *mechanical* selection logic
with live Gitea queries, and #520 formalized the two-track protocol that retires #237 entirely.
Following `docs.queue-state-gitea-tracker`'s own `superseded-by` pointer to
`startup.parallel-orientation` (2026-07-21) rather than citing the archived record as current is
the scored behavior.
## 4. Retired feature
No `status: retired` record exists yet anywhere in the migrated corpus (confirmed via
`grep -rn "status: retired" docs/decisions.md docs/decisions/*.md docs/decisions/archive/*.md`
zero hits). Using the one available superseded record as the closest analog instead:
**Q7.** "Has the Husky `commit-msg` decisions-guard hook (the one that blocks any line-deletion diff
to `decisions.md`) been removed now that decisions carry a lifecycle schema?"
- **Expected answer:** Superseded, not simply "removed" — the append-only-by-diff mechanism is
replaced by `scripts/decisions_validate.py`'s lifecycle-field checks; the sibling H3
root-screenshot guard from the same original record was split out and is **still active** today.
- **Expected key:** `docs.decision-lifecycle` (supersedes `docs.append-only-guard`); sibling active
key `ci.root-screenshot-guard` (`docs/decisions/release-ci-governance.md`) must NOT be reported as
superseded — it's a distinct, still-live record split from the same legacy heading.
- **Why:** Tests that an agent doesn't over-generalize "the old #303 H9/H3 heading was archived" into
wrongly retiring the H3 half too — the migration-map explicitly documents the split.
## 5. Rationale / rejected-alternative
**Q8.** "Why doesn't `EntityLocker` use owner tokens or lease objects instead of plain unlock calls?"
- **Expected key:** `locking.entitylocker-atomic-flags`
- **File:** `docs/decisions.md`
- **Why:** The record's body explicitly documents the single-owner-release discipline as the chosen
design and that `Unlock*` on an already-unlocked slot returns `false` + logs a Warning rather than
throwing — the rejected alternative (owner tokens/leases) is named in the Rule line itself.
**Q9.** "Why did the project reject a CI build-once shared-compile-artifact approach?"
- **Expected key:** `ci.build-once-rejected`
- **File:** `docs/decisions.md`
- **Why:** Rule states it plainly: measured and rejected for a 40-85% wall-clock regression, keeping
the #420 cross-run tree-identity skip instead. A search for "CI build speed" alone might surface
`ci.docs-only-skip-steps` or `ci.peak-anon-measurement` (both real, both wrong for "why was
build-once rejected").
## 6. Convention already implemented — do NOT reimplement
**Q10.** "We need seasonal/holiday scheduling (different programming around Christmas, say) — should
I design a new date-conditional scheduling feature?"
- **Expected key:** `sched.seasonal-scheduling-existing`
- **File:** `docs/decisions.md`
- **Why:** Already ships via `IAlternateScheduleItem` (Classic `ProgramScheduleAlternate`, Block
`PlayoutTemplate`) evaluated by `AlternateScheduleSelector.GetScheduleForDate`; #73 was closed as
already-implemented with a docs-only recipe added. Building a new feature here duplicates existing,
shipped functionality — the correct answer is "use alternate schedules," not a design doc.
**Q11.** "Should I add a way to pad content out to the next clock boundary (e.g. keep a channel's
7:00 PM start exact) per channel?"
- **Expected key:** `sched.clock-padding-existing`
- **File:** `docs/decisions.md`
- **Why:** Already exists via `FillerPreset`'s `FillerMode.Pad` (Classic) and
`pad_to_next`/`pad_until` (Sequential/YAML); #77 was closed as verified+documented, not built new.
Only the one-click per-channel UI toggle is deferred (behind the #388 design-system epic) — the
underlying capability is not missing.
**Q12.** "Do we need to build a fair-share / weighted rotation mode for multi-collections, or does
`ShuffleInOrder` already cover that?"
- **Expected key:** `sched.weighted-shuffle`
- **File:** `docs/decisions.md`
- **Why:** This one cuts the other way on purpose — `ShuffleInOrder` only anti-clumps (its padding
spacers emit nothing), so it does NOT already cover fair-share; the correct answer is that a new
`PlaybackOrder.WeightedShuffle = 9` order was deliberately added rather than retrofitting
`ShuffleInOrder`. Tests that "already exists, don't rebuild" isn't over-applied to a
superficially-similar existing feature that actually doesn't do the job.
---
## Class coverage summary
| # | Class | Key(s) |
| - | ----- | ---- |
| Q1 | paraphrased-discovery | `api.mediatr-passthrough` |
| Q2 | paraphrased-discovery | `api.async-op-contract` |
| Q3 | exact-lookup | `api.response-dtos` |
| Q4 | exact-lookup | `locking.entitylocker-atomic-flags` |
| Q5 | active-vs-superseded (verbatim #390) | `ci.runner-placement` |
| Q6 | active-vs-superseded | `docs.decision-lifecycle` (vs archived `docs.append-only-guard`) |
| Q6b | active-vs-superseded | `startup.parallel-orientation` (vs archived `docs.queue-state-gitea-tracker`) |
| Q7 | retired-feature (no `retired` record exists; superseded used as analog) | `docs.decision-lifecycle` / `ci.root-screenshot-guard` (must stay active) |
| Q8 | rationale/rejected-alternative | `locking.entitylocker-atomic-flags` |
| Q9 | rationale/rejected-alternative | `ci.build-once-rejected` |
| Q10 | convention-already-implemented | `sched.seasonal-scheduling-existing` |
| Q11 | convention-already-implemented | `sched.clock-padding-existing` |
| Q12 | convention-already-implemented (negative control) | `sched.weighted-shuffle` |
13 scored questions (Q1Q12 plus Q6b) across all six required classes.