168 records -> docs/decisions/records/<area>/<topic>.md (163 active, 23 dirs) and docs/decisions/archive/<area>/<topic>.md (5 archived). The filename IS the key, so one-active-record-per-key becomes a filesystem property rather than a validator check, and supersession becomes a `git mv`. WHY: the monolith was a concurrency problem before an aesthetic one. A 3,900-line append target made parallel sessions collide -- PR #605 and PR #614 both hit append-vs-append conflicts during routine rebases, and hand-resolving those inside the corpus is exactly the operation the rationale-rewrite guard exists to police. HOW IT IS VERIFIED: a ~170-file diff cannot be meaningfully read, so correctness does not rest on reading it. The parser was taught BOTH formats first, so the body-diff guard parses the old form at the merge-base and the new form at head -- the migration validates itself, no bypass. The proof is a field-level equivalence harness: 168 records before and after, zero lost, zero gained, zero field mismatches, zero rationale bodies differing. Reviewers should scrutinise the harness; it is the actual evidence. What measuring caught that reading would not have: - ~500 lines sit OUTSIDE any record -- decisions.md's lifecycle schema and each topic file's preamble, mostly the only copy. Source files are kept and stripped, never deleted. They also cannot be filed per-area: topic files hold several areas and 4 of 23 areas span several files. - Archive discovery was a non-recursive glob; after the split it found ZERO archived records, surfacing as four bogus "supersedes points to unknown key" errors rather than an obvious failure. - ~32 live docs point into the corpus BY DATE, which the split dangles. Each stripped file now ends with a generated "Records formerly in this file" index, which also rescues the identical breadcrumbs in old issue comments. - decisions.md's "In this file:" list was 97 same-file anchor bullets that the split makes WRONG, not merely stale. Dropped; the generated index replaces them with links that resolve. The equivalence harness now runs against a checked-in FIXTURE, not the live corpus. The earlier version migrated the real tree, which made it a one-shot: the moment the migration landed there was nothing left to move and the tests failed for reasons unrelated to the code. A fixture keeps them testing the SCRIPT rather than the repo's current state. Keys preserved verbatim, warts included: `sched` (12) and `scheduling` (1) remain two directories for one concept. Renaming a key is not a move -- it changes identity, breaks the equivalence proof, and invalidates MemPalace's per-key drawers. Taxonomy normalisation is separate work. refs #610
3.5 KiB
key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
| key | title | status | since | supersedes | superseded-by | rule | signals | mechanics |
|---|---|---|---|---|---|---|---|---|
| api.scheduling-hardening | 2026-07-13 — Scheduling API hardening: null-name 500s, duplicate template items, unreachable 404 (#172) | active | 2026-07-13 | none | none | Create/Replace handlers guard against null/whitespace `name` (`IsNullOrWhiteSpace`, not just `Length`) to prevent NRE-500s, template-item overlap validation compares by index (not record value-equality) to catch exact-duplicate items, and unreachable 404 `ProducesResponseType` attributes on create-only actions are trimmed. | `BlockTemplateItem` record value-equality bypass, `api-conventions.md` §3b handler-hardening checklist · paths: `ReplaceTemplateItemsHandler`, `api-conventions.md` §3b · issues: #172, #144 | `docs/api-conventions.md` §3b |
Cleared the still-live findings from issue #172 (consolidated non-blocking nits from the #144 S1/S2
reviews). Most of the 2026-07-07 list had already been ratified deliberate (§8 "(none)" synthesized
rows; §3b deep-FK non-existence-check) or fixed since (the unauthenticated /api/logs +
/api/troubleshoot/info GETs now carry [RequiresAuthentication] per §9; the Trakt matched-items link
points at the live /app/search; GET /api/search already fans out via Task.WhenAll). Three were
genuinely live:
- Null/empty
name→ 500 (10 handlers). Create + Replace/Update handlers for Block, Template, DecoTemplate, Deco (8, all genuine 500s), plusUpdateFFmpegProfile(genuine 500;CreateFFmpegProfilewas already guarded) andCreatePlaylist(its DTO coalescesnull→"", so an empty-name persist, not a 500) all didif (request.Name.Length > 50)on a client-nullablestring Name→ unhandledNullReferenceException. Fixed toif (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 50)— kills the NRE, and also rejects empty/whitespace names (matching the group-create handlers'NotEmptybehavior, closing a latent "block/template named ''" gap). Chose the one-line guard over refactoring each handler onto theNotEmpty/NotLongerThancombinator to keep the blast radius tiny and preserve each handler's existing error message + 422 mapping. Convention captured inapi-conventions.md§3b handler-hardening checklist. - Exact-duplicate template items bypassed overlap validation.
ReplaceTemplateItemsHandler's O(n²) overlap loop skipped onitem == otherItem, butBlockTemplateItemis arecord, so two value-identical items (same BlockId + StartTime → same computed EndTime) were value-equal and skipped — both persisted unvalidated. Switched to index-based iteration (i != j) so identical items at distinct positions are compared and register as a (self-)intersection → rejected 422. (The SPA's index-based check already caught this client-side; it was an API-only gap.) - Unreachable 404 on create-group actions.
POST /api/blocks/groupsandPOST /api/templates/groupsdeclared[ProducesResponseType(ProblemDetails, 404)]copied from precedent, but a create has no parent lookup that can 404 (only 201/422). Trimmed — OpenAPI spec regenerated.
Deliberately not fixed (documented as accepted): the §8 "(none)" synthetic rows, the §3b deep-FK
non-existence-check, and the missing Name= on PlayoutController Create/Delete/Update (moot — the
"v1"-doc OperationIdOpenApiTransformer (#197 Bundle C) already synthesizes stable operationIds for
Name=-less ops, and adding Name= would risk renaming generated SPA client methods). Refs #172 #197.