docs(568): the rule the branch establishes gets a record, and every row the fix moved is re-measured
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 7s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
PR Gates / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / decisions lifecycle (pull_request) Successful in 18s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 12s
Review verdict / Set review-verdict status (pull_request_target) Successful in 26s
review-verdict/h10 Review-verdict: MERGEABLE @ fcdc381 (base: main)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m23s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 17m33s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 7s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
PR Gates / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / decisions lifecycle (pull_request) Successful in 18s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 12s
Review verdict / Set review-verdict status (pull_request_target) Successful in 26s
review-verdict/h10 Review-verdict: MERGEABLE @ fcdc381 (base: main)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m23s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 17m33s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
api-conventions.md now says which half of an id-list validator a sibling field may gate (existence, never the raw-count cap) and that a lost-race recovery re-asks the whole validator set rather than the fields whoever wrote the catch remembered. Those, with the bound and the field-named 422, are one convention with residuals, so they get a record -- api.top-level-id-list-validation -- and a task-signal row. The record states what #568 does NOT settle: three validators on two DTOs is a per-field constant, not the repo-wide rule #917 owns, and it says to expect #917 to replace the mechanism. graphics-elements.md: rows 35-44 re-measured against the whole ErsatzTV.Tests project on this tree, because the fix moved five of their red sets -- Validate is now also what the recovery path re-runs, so removing a validator from it reddens that handler's race test too. Rows 45-47 are new and measured the same way. The "redden more than one test" figure is recounted from the table (21 -> 24); the cross-fixture set is unchanged at five. The negative discriminator rows now carry a stated seeding rule: vary one half of the identity and hold the other at the seeded value. Varying both leaves the row rejected by the pre-#568 predicate as well, so a composite revert to it would pass every test at that site. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
This commit is contained in:
@@ -30,6 +30,7 @@ doc below, or that changes which sections a task signal points to.**
|
||||
| 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 — and so does a PROSE claim that some mutation reddens, or does not redden, a named test, wherever it is written: it is a `CLAIMS` entry in the same manifest, bound to its site and verbatim quote, or it is not written) — 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. Then `…/guard-pins-the-artifact-not-a-shape.md` for the SHAPE of the predicate itself: over an artifact whose GRAMMAR the predicate does not implement, pin the artifact WHOLE by default — matching a shape inside it is the exception and must carry that record's four-part argument; that record's `rule:` is the one place the qualifying grammars are enumerated |
|
||||
| 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` |
|
||||
| Adding or changing a write path that takes a top-level LIST of FK ids (`graphicsElementIds`, `watermarkIds`) | `docs/api-conventions.md` §3b — bound the RAW list through `Validators.IdsMustExist` before `Distinct`, reject an unknown id with a 422 that names the request field, gate only the EXISTENCE half on whatever makes the apply path discard the list, and translate a lost race by re-running the handler's whole `Validate` on a fresh context. Then `api.top-level-id-list-validation`; the repo-wide rollout is #917 |
|
||||
| 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) |
|
||||
|
||||
+22
-10
@@ -307,7 +307,12 @@ handler's validation when a lookup fails, so the controller-side mapping falls o
|
||||
`Validators.IdsMustExist` (#568), which counts the **raw** list before `Distinct` and before any
|
||||
database work — deduplication is not what the request costs to parse and materialize — and caps how
|
||||
many rejected ids the 422 echoes back, so an oversized request does not earn an oversized response.
|
||||
The rollout to the other FK-id validators on these DTOs is #917.
|
||||
The count is the REQUEST's bound, not the apply path's: where another field of the same body (a
|
||||
deco's `DecoMode`) makes the reconcile discard the list, the mode gates the EXISTENCE half only —
|
||||
a discarded list was still parsed and materialized out of the body. Pass that predicate to
|
||||
`IdsMustExist` as `idsAreConsumed` rather than short-circuiting the call, which takes the cap with
|
||||
it. The rollout to the other FK-id validators on these DTOs is #917; the rule and its residuals are
|
||||
`api.top-level-id-list-validation`.
|
||||
- **Name the request field in the validation message**, `[GraphicsElementIds] …`, using the same
|
||||
`[{GetMemberName(expression)}]` prefix `Validators.NotEmpty`/`NotLongerThan`/`AtLeast` already
|
||||
produce. A full-replace DTO carries several id lists, and `Graphics element(s) do not exist: 42`
|
||||
@@ -316,10 +321,13 @@ handler's validation when a lookup fails, so the controller-side mapping falls o
|
||||
`SaveChangesAsync`** — a background job deleting the principal row (e.g. `RefreshGraphicsElements`
|
||||
removing elements whose template file is gone) restores the very 500 the validator was added to
|
||||
prevent. A transaction does not close that window: neither provider locks the rows the validator
|
||||
merely READ. Catch `DbUpdateException` around the save, re-ask the existence question on a **fresh**
|
||||
context (the failed one still tracks the changes it could not write), and return the validator's own
|
||||
422 if an id has since gone — re-throwing anything else, so a real fault is not reported as a client
|
||||
error (#568).
|
||||
merely READ. Catch `DbUpdateException` around the save, re-ask the question on a **fresh** context
|
||||
(the failed one still tracks the changes it could not write), and return the validator's own 422 if
|
||||
an id has since gone — re-throwing anything else, so a real fault is not reported as a client error
|
||||
(#568). **Re-run the handler's whole `Validate`, never a named subset of its fields**: every FK the
|
||||
save writes loses the same race, and a recovery path that enumerates the fields it knows about
|
||||
silently omits the next one the DTO gains. Re-running the validator set covers a check added later
|
||||
by construction.
|
||||
- **Dereferencing a request `string` (e.g. `request.Name.Length`) is a latent 500** — request DTOs
|
||||
carry no `#nullable` context (§2), so a `string Name` binds `null` from `name: null`/an omitted field
|
||||
and there is no implicit `[Required]`; a raw `.Length`/`.Trim()` throws `NullReferenceException` → an
|
||||
@@ -961,13 +969,17 @@ every other field on this full-replace DTO. The identical shape existed on `PUT
|
||||
(`ReplaceDecoRequest.graphicsElementIds`/`watermarkIds`, also top-level fields, not the
|
||||
`deep-FK-in-a-nested-list` carve-out of §3b above) and is hardened the same way by
|
||||
`UpdateDecoHandler.Validate` (`GraphicsElementIdsMustExist`/`WatermarkIdsMustExist`, #568). Each
|
||||
deco validator is gated on the same `Override`/`Merge` mode predicate that makes the apply path
|
||||
consume its id list — under `Inherit`/`Disable` the reconcile clears the join and ignores the ids,
|
||||
so validating them there would 422 a request over ids it was about to discard. The SPA sends both
|
||||
id lists whatever the mode selector says, so that shape arrives from the real editor.
|
||||
deco validator passes the same `Override`/`Merge` mode predicate that makes the apply path consume
|
||||
its id list — under `Inherit`/`Disable` the reconcile clears the join and ignores the ids, so
|
||||
validating them there would 422 a request over ids it was about to discard. The SPA sends both id
|
||||
lists whatever the mode selector says, so that shape arrives from the real editor. The predicate is
|
||||
an argument (`idsAreConsumed`) rather than a short-circuit around the call, because it gates the
|
||||
existence half alone and the raw-count cap applies under every mode.
|
||||
All three id lists go through one shared validator, `Validators.IdsMustExist`, which is where the
|
||||
count cap (`MaximumIdListCount`), the request field named in the message and the cap on echoed ids
|
||||
are written once rather than three times.
|
||||
are written once rather than three times. Both handlers translate a lost race the same way, by
|
||||
re-running their own `Validate` on a fresh context after a `DbUpdateException` — so every FK on the
|
||||
DTO is covered, not just the id lists this issue was about.
|
||||
`GraphicsElementResponseModel` exposes a server-derived `builtIn`, computed by
|
||||
`GetAllGraphicsElementsForApiHandler` as
|
||||
`GraphicsElementDefaults.IsOnNowNext(element.Path, element.Kind)` — ordinal equality against the full
|
||||
|
||||
@@ -35,6 +35,7 @@ the link for rationale. Superseded/retired history lives in `archive/`. Regenera
|
||||
| `api.search-field-values-unicode-fold` | The EF-sourced facet fields (`genre`, `show_genre`, `studio`, `director`, `writer`, `actor`, `tag`, `network`, `collection`, `video_codec`, `album`, and `artist`'s entity half) reach stored values whose prefix carries an uppercase non-ASCII character, on BOTH providers, with no row budget and no accepted loss. The defect was SQLite-only and ONE-SIDED: SQLite's `LOWER()` folds ASCII only (`lower('Édith')` is `'Édith'` unchanged), so the predicate UNDER-matched, which no later stage can repair. MySQL was already correct — its `LOWER()` is Unicode-aware, so `LOWER('Édith')` really is `'édith'` and the existing predicate reaches the row unaided. The fix is a SECOND, ADDITIVE query taken only when `isSqlite && q contains a non-ASCII character`: raw Dapper SQL `SELECT DISTINCT <col> AS Value FROM <table> WHERE [<discriminator> AND] etv_upper(<col>) LIKE @Pattern ESCAPE '\' ORDER BY <col> LIMIT @Limit`, where `etv_upper` is a `SqliteConnection.CreateFunction` scalar implementing `ToUpperInvariant`. Every other case — all-ASCII `q`, and MySQL for all `q` — runs today's EF query BYTE-IDENTICALLY. Keeping selectivity in SQL here is NOT the refuted family from `api.search-field-values-sources`: those four attempts bounded a walk around a predicate that could not be made correct over JSON escape text, whereas this is a correct fold on a plain column in an ordinary `LIMIT`ed query. It narrows that record's "Known limitation inherited, not introduced" clause; everything else it settles still holds. | 2026-07-27 | [link](records/api/search-field-values-unicode-fold.md) |
|
||||
| `api.search-paging-cap` | Search stays capped at 100 items per media kind; an overflowing kind's "See all" reuses library-browse paging instead of adding new API surface. | 2026-07-11 | [link](records/api/search-paging-cap.md) |
|
||||
| `api.selection-projection-include-chain` | Every handler that projects an aggregate carrying a tagged-union selection loads it through ONE shared `<Aggregate>QueryExtensions` include chain — `RerunCollectionQueryExtensions.IncludeSelectionDetails()`, joining the existing `ProgramScheduleItemQueryExtensions.IncludeScheduleItemDetails()` — called by the paged-list handler and the by-id handler alike, so the two cannot drift. The media-item flattening switch is likewise ONE shared helper, `MediaCollections.Mapper.ProjectMediaItemToViewModel`, covering all ten selectable media types including `RemoteStream`, whose named projection is `MediaItems.Mapper.ProjectToNamedViewModel` (it cannot be an overload of `ProjectToViewModel(RemoteStream)`, which already exists returning the unrelated `RemoteStreamViewModel`; C# will not overload on return type). That switch NEVER ends in `_ => null`: a null MediaItem is the legitimate not-a-media-item case, while an unrecognized non-null subtype keeps its id and takes a conspicuous `[unsupported media type: X]` name. Fail-soft is deliberate — throwing would fail an entire paged GET over one unreadable row. Finally, every metadata navigation inside `MediaItems.Mapper` is read through `Optional(...).Flatten()` and degrades to the `"???"` placeholder, because those projections are reached from handlers whose include chains differ and a bare `x.Season.Show.ShowMetadata` is a latent 500 on some other caller GET. | 2026-07-28 | [link](records/api/selection-projection-include-chain.md) |
|
||||
| `api.top-level-id-list-validation` | A list of foreign-key ids that is a TOP-LEVEL field of a full-replace write DTO (`UpdateChannelRequest.graphicsElementIds`, `ReplaceDecoRequest.graphicsElementIds`/`watermarkIds`) is validated like every other FK field on that DTO, through the one shared primitive `Validators.IdsMustExist`. Four properties, in this order: (1) the RAW list length is capped at `Validators.MaximumIdListCount` before `Distinct` and before any database work, because deduplication is not what the request cost to parse and materialize; (2) an id with no matching row is a 422, never the FK constraint reaching `SaveChangesAsync` as an unhandled 500; (3) the message carries the request field name in brackets, `[GraphicsElementIds] ...`, the prefix `Validators.NotEmpty`/`NotLongerThan`/`AtLeast` already produce, and echoes at most ten missing ids plus a count; (4) where a SIBLING field of the same request makes the apply path discard the list — a deco `DecoMode` outside `Override`/`Merge` — that predicate is passed in as `idsAreConsumed` and gates the EXISTENCE half ONLY. It never gates the cap: a discarded list was parsed and materialized out of the request body all the same. Because validation and the write are two statements, a handler that writes any FK also catches `DbUpdateException` around the save and re-runs ITS OWN `Validate` on a FRESH context, returning the validator's own 422 when a principal row has since gone and re-throwing otherwise. The whole validator set is re-asked, never a named subset of fields. This is NOT the deep-FK carve-out of api-conventions.md 3b, which stays: ids nested inside item-list request bodies are still not existence-checked at that depth. | 2026-09-05 | [link](records/api/top-level-id-list-validation.md) |
|
||||
| `api.versioning-v1` | The entire `/api` surface is versioned to `/api/v1` uniformly (no unversioned corner); legacy unversioned callers are rewritten in-pipeline (not redirected) with Deprecation/Link/Sunset headers, and post-freeze `/api/v1` is additive-only — a breaking change requires `/api/v2`. | 2026-07-13 | [link](records/api/versioning-v1.md) |
|
||||
| `blazor.rollback-tag` | The commit immediately preceding the Blazor-removal merge is tagged `blazor-final` (not a `v*` tag, so it doesn't trigger a prod release build) as the documented rollback/restore path. | 2026-07-11 | [link](records/blazor/rollback-tag.md) |
|
||||
| `blazor.ui-removed` | The legacy Blazor Server UI (`Pages/`, `Shared/`, `ViewModels/`, `Validators/`, MudBlazor + 8 other packages, Blazor Startup wiring) is fully deleted now that the SPA has parity; the legacy `MapWhen` branch is kept only for controllers/docs/OpenAPI/`LegacyUiRedirects`, and the catch-all fallback 302s any unmatched non-api/artwork/docs/openapi path to `/app`. | 2026-07-11 | [link](records/blazor/ui-removed.md) |
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
key: api.top-level-id-list-validation
|
||||
title: '2026-09-05 — A top-level list of FK ids on a full-replace write DTO is bounded, existence-checked, named in its own 422, and re-asked after a lost race (#568)'
|
||||
status: active
|
||||
since: '2026-09-05'
|
||||
supersedes: none
|
||||
superseded-by: none
|
||||
rule: 'A list of foreign-key ids that is a TOP-LEVEL field of a full-replace write DTO (`UpdateChannelRequest.graphicsElementIds`, `ReplaceDecoRequest.graphicsElementIds`/`watermarkIds`) is validated like every other FK field on that DTO, through the one shared primitive `Validators.IdsMustExist`. Four properties, in this order: (1) the RAW list length is capped at `Validators.MaximumIdListCount` before `Distinct` and before any database work, because deduplication is not what the request cost to parse and materialize; (2) an id with no matching row is a 422, never the FK constraint reaching `SaveChangesAsync` as an unhandled 500; (3) the message carries the request field name in brackets, `[GraphicsElementIds] ...`, the prefix `Validators.NotEmpty`/`NotLongerThan`/`AtLeast` already produce, and echoes at most ten missing ids plus a count; (4) where a SIBLING field of the same request makes the apply path discard the list — a deco `DecoMode` outside `Override`/`Merge` — that predicate is passed in as `idsAreConsumed` and gates the EXISTENCE half ONLY. It never gates the cap: a discarded list was parsed and materialized out of the request body all the same. Because validation and the write are two statements, a handler that writes any FK also catches `DbUpdateException` around the save and re-runs ITS OWN `Validate` on a FRESH context, returning the validator''s own 422 when a principal row has since gone and re-throwing otherwise. The whole validator set is re-asked, never a named subset of fields. This is NOT the deep-FK carve-out of api-conventions.md 3b, which stays: ids nested inside item-list request bodies are still not existence-checked at that depth.'
|
||||
signals: 'unknown graphicsElementIds returns 500 · unbounded client-supplied id array · which id list does this 422 mean · FK deleted between validation and SaveChangesAsync · deco mode gate skips the count cap · IdsMustExist · MaximumIdListCount · idsAreConsumed · ApplyUpdateRequestTranslatingLostRace · paths: `ErsatzTV.Application/Validators/IdListValidation.cs`, `ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs`, `ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs`, `docs/api-conventions.md` 3b · issues: #568, #917, #74'
|
||||
mechanics: '`Validators.IdsMustExist<T>(input, expression, noun, idsAreConsumed, findExisting, ct)` in `ErsatzTV.Application/Validators/IdListValidation.cs`; the field name comes from `GetMemberName(expression)`, so it cannot drift from the DTO. Pinned by `UpdateChannelGraphicsElementsTests` and `UpdateDecoGraphicsElementsTests`; the clause mutations and their measured red sets are rows 35-47 of the table in `docs/graphics-elements.md`.'
|
||||
---
|
||||
|
||||
- **The cap belongs to the request, the existence check belongs to the apply path.** These are two
|
||||
different questions and the first attempt at the mode gate conflated them: the deco validators
|
||||
short-circuited the whole `IdsMustExist` call when `DecoMode` was `Inherit`/`Disable`, which was
|
||||
right about existence and wrong about the count. A validator may not reject an id the reconcile
|
||||
was going to throw away — the SPA sends both id lists whatever the mode selector says, so a draft
|
||||
holding a since-deleted element must still be able to save the deco back to `Inherit` — but the
|
||||
array was still bound, allocated and materialized before any of that was known. Hence
|
||||
`idsAreConsumed` as a parameter of the shared primitive rather than an `if` around the call site:
|
||||
the primitive decides what the flag may switch off, and no call site can widen it.
|
||||
|
||||
- **A recovery path that names its own fields is the defect it is recovering from.** Re-asking only
|
||||
the graphics-element question after a `DbUpdateException` left `WatermarkId`, `FFmpegProfileId`,
|
||||
`FallbackFillerId` and `MirrorSourceChannelId` — every one of them written by the same
|
||||
`SaveChangesAsync`, every one of them a real FK — surfacing the same unhandled 500 the catch exists
|
||||
to remove. Re-running the handler's own `Validate` is what makes the coverage derive from the
|
||||
validator set instead of from whoever last edited the catch. A transaction is not an alternative:
|
||||
neither provider locks rows the validator merely READ, so the concurrent delete commits either way.
|
||||
The context must be a fresh one — the failed context still tracks the changes it could not write,
|
||||
and would answer the existence query out of them.
|
||||
|
||||
- **Bounded, but only here — and deliberately.** Three validators on two DTOs carry this cap; the
|
||||
other FK validators on the same DTOs, and the `O(existing x desired)` reconcile loops the ids then
|
||||
feed, do not. That is not an oversight and it is not a finished rule: capping three sites and not
|
||||
the rest is a per-field constant, and #917 owns the repo-wide question (where the bound lives, how
|
||||
the limit is derived, and bounding the apply paths as well as the validators). Cite this record for
|
||||
the SHAPE a bounded id list takes; expect #917 to replace the mechanism.
|
||||
|
||||
- **`MaximumIdListCount` is a ceiling on abuse, not a product limit.** 512, against tables an
|
||||
operator curates by hand where a few dozen rows is a large install. Nothing reachable from the
|
||||
editor can approach it, which is what lets it be a hard rejection rather than a clamp.
|
||||
|
||||
- **A 422 that echoes every rejected id turns an oversized request into an oversized response.** Ten
|
||||
ids and a count: enough to fix the payload by hand, bounded by construction.
|
||||
@@ -148,6 +148,13 @@ test asserting such a row is ignored therefore cannot appear in row 18's red set
|
||||
cannot fail, and the combination is deliberately not shipped. Row 10 drops the identity call
|
||||
entirely, which is a different mutation from dropping either half.
|
||||
|
||||
The same arithmetic constrains how each negative case is SEEDED, at both discriminator sites: a
|
||||
row varies exactly one half of the identity and holds the other at the seeded value. The
|
||||
outside-folder rows carry `Kind == Text`, the wrong-kind rows sit at the seeded path. A negative row
|
||||
that varies both is refused by the surviving half of the *pre-#568* predicate too — filename plus
|
||||
`Kind == Text` — so a composite revert to it would pass every discriminator test at that site while
|
||||
restoring the folder-agnostic bug the issue reports (#568).
|
||||
|
||||
`IsOnNowNext` is **ordinal**, and every caller applies it **in memory** rather than in a `Where`
|
||||
clause. That is not incidental: `GraphicsElement.Path` takes no explicit collation (`TvContext`
|
||||
pins one only on the listed name/title columns), so a SQL `Path ==` comparison is case-sensitive
|
||||
@@ -179,7 +186,7 @@ Three traps this table is built to avoid:
|
||||
|
||||
- **Measure against the whole project, never a per-fixture filter.** A filtered run structurally
|
||||
cannot observe a red in another fixture, so it under-reports the failure set while looking precise.
|
||||
Twenty-one rows below redden more than one test, and five of them (13, 18, 22, 33, 41) span two
|
||||
Twenty-four rows below redden more than one test, and five of them (13, 18, 22, 33, 41) span two
|
||||
fixture classes — those five are exactly what a per-fixture filter cannot see. Both figures are
|
||||
counted from the table itself, so a row added without recounting them makes this bullet quietly
|
||||
false — recount both whenever a row is added or its red set changes.
|
||||
@@ -261,13 +268,16 @@ red.
|
||||
| 32 | the lineup create path stops attaching the built-in element | `Should_Attach_The_Built_In_On_Now_Next_Element` |
|
||||
| 33 | `IsOnNowNext` loosened from `Ordinal` to `OrdinalIgnoreCase` (#568) — one row for both discriminator sites, since they share the predicate | `GetAllGraphicsElementsForApi_Should_Not_Mark_A_Case_Variant_Of_The_Seeded_Path_As_BuiltIn`<br>`Ignores_A_Case_Variant_Of_The_Seeded_Path` |
|
||||
| 34 | `EnsureBuiltInElementRow`'s existence check re-derived as a SQL `AnyAsync(e => e.Path == target)` instead of asking `GetBuiltInElementId` (#568) | `A_Row_Of_Another_Kind_At_The_Seeded_Path_Does_Not_Suppress_The_Built_In_Row` |
|
||||
| 35 | `GraphicsElementIdsMustExist` removed from `UpdateChannelHandler.Validate` (#568) — the unknown-id 422 the issue asks for, and with it the count cap and the diagnostic cap that validator carries | `Should_Cap_The_Ids_Echoed_Back_In_The_Unknown_Id_422`<br>`Should_Reject_A_Duplicate_Heavy_List_On_Its_Raw_Count`<br>`Should_Reject_One_More_Than_The_Maximum_Number_Of_GraphicsElementIds`<br>`Should_Reject_Unknown_GraphicsElementId_With_422_Not_500`<br>`Should_Reject_When_One_Of_Several_GraphicsElementIds_Is_Unknown` |
|
||||
| 36 | `GraphicsElementIdsMustExist` removed from `UpdateDecoHandler.Validate` (#568) | `Should_Reject_More_Than_The_Maximum_Number_Of_GraphicsElementIds`<br>`Should_Reject_Unknown_GraphicsElementId_With_A_Validation_Error_Not_A_Throw` |
|
||||
| 37 | `WatermarkIdsMustExist` removed from `UpdateDecoHandler.Validate` (#568) | `Should_Reject_More_Than_The_Maximum_Number_Of_WatermarkIds`<br>`Should_Reject_Unknown_WatermarkId_With_A_Validation_Error_Not_A_Throw` |
|
||||
| 38 | the `ConsumesGraphicsElementIds` mode gate removed from `UpdateDecoHandler.GraphicsElementIdsMustExist` (#568) — a validator may only reject ids the apply path would consume | `Should_Ignore_An_Unknown_GraphicsElementId_When_The_Mode_Does_Not_Consume_It` |
|
||||
| 39 | the `ConsumesWatermarkIds` mode gate removed from `UpdateDecoHandler.WatermarkIdsMustExist` (#568) | `Should_Ignore_An_Unknown_WatermarkId_When_The_Mode_Does_Not_Consume_It` |
|
||||
| 35 | `GraphicsElementIdsMustExist` removed from `UpdateChannelHandler.Validate` (#568) — the unknown-id 422 the issue asks for, and with it the count cap and the diagnostic cap that validator carries, and the lost-race recheck, which re-runs `Validate` whole | `Should_Cap_The_Ids_Echoed_Back_In_The_Unknown_Id_422`<br>`Should_Reject_A_Duplicate_Heavy_List_On_Its_Raw_Count`<br>`Should_Reject_One_More_Than_The_Maximum_Number_Of_GraphicsElementIds`<br>`Should_Reject_Unknown_GraphicsElementId_With_422_Not_500`<br>`Should_Reject_When_One_Of_Several_GraphicsElementIds_Is_Unknown`<br>`Should_Translate_An_Element_Deleted_Between_Validation_And_Save_Into_The_Same_422` |
|
||||
| 36 | `GraphicsElementIdsMustExist` removed from `UpdateDecoHandler.Validate` (#568) — and so from the lost-race recheck too, which re-runs `Validate` whole | `Should_Reject_More_Than_The_Maximum_Number_Of_GraphicsElementIds`<br>`Should_Reject_Too_Many_GraphicsElementIds_Even_Under_A_Mode_That_Does_Not_Consume_Them`<br>`Should_Reject_Unknown_GraphicsElementId_With_A_Validation_Error_Not_A_Throw`<br>`Should_Translate_A_Deco_Element_Deleted_Between_Validation_And_Save_Into_The_Same_422` |
|
||||
| 37 | `WatermarkIdsMustExist` removed from `UpdateDecoHandler.Validate` (#568) — and so from the lost-race recheck too | `Should_Reject_More_Than_The_Maximum_Number_Of_WatermarkIds`<br>`Should_Reject_Too_Many_WatermarkIds_Even_Under_A_Mode_That_Does_Not_Consume_Them`<br>`Should_Reject_Unknown_WatermarkId_With_A_Validation_Error_Not_A_Throw`<br>`Should_Translate_A_Deco_Watermark_Deleted_Between_Validation_And_Save_Into_The_Same_422` |
|
||||
| 38 | `idsAreConsumed: ConsumesGraphicsElementIds(request)` replaced by `idsAreConsumed: true` at `UpdateDecoHandler.GraphicsElementIdsMustExist`'s call site (#568) — a validator may only reject ids the apply path would consume | `Should_Ignore_An_Unknown_GraphicsElementId_When_The_Mode_Does_Not_Consume_It` |
|
||||
| 39 | the same replacement at `UpdateDecoHandler.WatermarkIdsMustExist`'s call site (#568) | `Should_Ignore_An_Unknown_WatermarkId_When_The_Mode_Does_Not_Consume_It` |
|
||||
| 40 | `GetAllGraphicsElementsForApiHandler`'s `BuiltIn` reverted from `GraphicsElementDefaults.IsOnNowNext(e.Path, e.Kind)` to `Path.GetFileName(e.Path) == GraphicsElementDefaults.OnNowNextFileName` (#568) — the API-side call, the folder-agnostic and kind-agnostic shape the issue reports | `GetAllGraphicsElementsForApi_Should_Not_Mark_A_Wrong_Kind_Row_At_The_Seeded_Path_As_BuiltIn`<br>`GetAllGraphicsElementsForApi_Should_Not_Mark_Same_Filename_Outside_Seeded_Folder_As_BuiltIn` |
|
||||
| 41 | the raw-count cap removed from `Validators.IdsMustExist` (#568) — one clause, three call sites, so its red set is what shows the cap is shared rather than restated | `Should_Reject_A_Duplicate_Heavy_List_On_Its_Raw_Count`<br>`Should_Reject_More_Than_The_Maximum_Number_Of_GraphicsElementIds`<br>`Should_Reject_More_Than_The_Maximum_Number_Of_WatermarkIds`<br>`Should_Reject_One_More_Than_The_Maximum_Number_Of_GraphicsElementIds` |
|
||||
| 41 | the raw-count cap removed from `Validators.IdsMustExist` (#568) — one clause, three call sites, so its red set is what shows the cap is shared rather than restated | `Should_Reject_A_Duplicate_Heavy_List_On_Its_Raw_Count`<br>`Should_Reject_More_Than_The_Maximum_Number_Of_GraphicsElementIds`<br>`Should_Reject_More_Than_The_Maximum_Number_Of_WatermarkIds`<br>`Should_Reject_One_More_Than_The_Maximum_Number_Of_GraphicsElementIds`<br>`Should_Reject_Too_Many_GraphicsElementIds_Even_Under_A_Mode_That_Does_Not_Consume_Them`<br>`Should_Reject_Too_Many_WatermarkIds_Even_Under_A_Mode_That_Does_Not_Consume_Them` |
|
||||
| 42 | `Validators.DescribeIds` stops truncating and joins every missing id (#568) | `Should_Cap_The_Ids_Echoed_Back_In_The_Unknown_Id_422` |
|
||||
| 43 | the `DbUpdateException` catch disarmed in `UpdateChannelHandler.ApplyUpdateRequestTranslatingLostRace` (#568) — retyped to an exception the save cannot raise, so the FK failure propagates as it did before the fix | `Should_Translate_An_Element_Deleted_Between_Validation_And_Save_Into_The_Same_422` |
|
||||
| 44 | the same catch disarmed in `UpdateDecoHandler.ApplyUpdateRequestTranslatingLostRace` (#568) | `Should_Translate_A_Deco_Element_Deleted_Between_Validation_And_Save_Into_The_Same_422` |
|
||||
| 43 | the `DbUpdateException` catch disarmed in `UpdateChannelHandler.ApplyUpdateRequestTranslatingLostRace` (#568) — retyped to an exception the save cannot raise, so the FK failure propagates as it did before the fix | `Should_Translate_A_Watermark_Deleted_Between_Validation_And_Save_Into_The_Same_422`<br>`Should_Translate_An_Element_Deleted_Between_Validation_And_Save_Into_The_Same_422` |
|
||||
| 44 | the same catch disarmed in `UpdateDecoHandler.ApplyUpdateRequestTranslatingLostRace` (#568) | `Should_Translate_A_Deco_Element_Deleted_Between_Validation_And_Save_Into_The_Same_422`<br>`Should_Translate_A_Deco_Watermark_Deleted_Between_Validation_And_Save_Into_The_Same_422` |
|
||||
| 45 | the lost-race recheck in `UpdateChannelHandler.ApplyUpdateRequestTranslatingLostRace` narrowed from `Validate` back to `GraphicsElementIdsMustExist` alone (#568) — the recovery enumerating its own fields instead of re-asking the validator set, which omits every other FK the same save writes | `Should_Translate_A_Watermark_Deleted_Between_Validation_And_Save_Into_The_Same_422` |
|
||||
| 46 | the same narrowing in `UpdateDecoHandler.ApplyUpdateRequestTranslatingLostRace` (#568) | `Should_Translate_A_Deco_Watermark_Deleted_Between_Validation_And_Save_Into_The_Same_422` |
|
||||
| 47 | the `idsAreConsumed` gate hoisted ABOVE the raw-count cap in `Validators.IdsMustExist` (#568) — the shape the deco mode gate had while it short-circuited the whole call, which took the cap with it | `Should_Reject_Too_Many_GraphicsElementIds_Even_Under_A_Mode_That_Does_Not_Consume_Them`<br>`Should_Reject_Too_Many_WatermarkIds_Even_Under_A_Mode_That_Does_Not_Consume_Them` |
|
||||
|
||||
Reference in New Issue
Block a user