dafa56452c3b40ce97d5cbef42671eaa99b77fa6
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dafa56452c |
test(807): make "threaded through every branch" a checked property
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 11s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 40s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 45s
PR Gates / decisions lifecycle (pull_request) Successful in 23s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 6m0s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m49s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Review verdict / Set review-verdict status (pull_request_target) Successful in 9s
review-verdict/h10 Review-verdict: MERGEABLE @ dafa564 (base: main)
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m45s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
`_resolve` returns three sets and the third has to reach every composition site. That was a claim in a docstring; each site is a separate opportunity to forget, and forgetting is silent — no error, just a smaller population. Ten sites enumerated and asserted: top level, `allOf` arm, `if`, `then`, `else`, `dependentSchemas`, `oneOf` arm, `anyOf` arm, `items`, `prefixItems`. All ten currently thread it. Witnessed load-bearing by dropping the threading at three of them — `allOf` reddens 1 case, `oneOf`/`anyOf` 2, `items` 2. The value is not today's ten passes; it is that a NEW composition keyword has one visible place to be added to, instead of the omission being spread across the resolver where nothing would report it. 956 passed 2 skipped, ruff check 0, ruff format 0. |
||
|
|
424ffb573b |
fix(807): remove the collision class instead of asserting it away
Round 7 reviewed the assertion added two commits ago and found it covered 1 of
the 5 nodes where the collision fires — the four it missed being the ones that
matter, since they are where the walk actually qualifies a name. I had already
widened it to walk the whole tree, but the reviewer's recommendation is better
than either version and is taken here: restructure so there is nothing to assert.
A nested inline object lives in its OWN namespace. The defect was qualifying its
members as `parent.child` BEFORE subtracting `required`, which merges the two
namespaces and lets a literal `a.b` member mask a nested `a` -> `b` one. Now
`_optional_of` subtracts inside each namespace and qualifies only the survivors,
so the sets are never mixed. No separator to defend, no assumption to assert, and
`NESTED_SEPARATOR` and its whole-tree checker are gone.
Verified at all five sites — top level of a named schema, one level down, inside
an `allOf` arm, under `items`, and inside an inline request body — every one now
surfaces (`a.b`, `outer.a.b`, `a.b`, `a.b`, `a.b`); four were masked before.
The reviewer flagged a trap in this fix and it is real: keeping a 2-tuple
compatibility shim (`return properties | nested, required`) re-creates the mask
at the merge boundary while the suite stays green. So `_resolve` returns three
sets and every caller changed. There is now a case that reddens exactly that
shim.
Residual, stated rather than hidden: a literal `a.b` and a nested `a` -> `b` that
are BOTH optional conflate into one reported string. That is a diagnostic
ambiguity, not a miss — the schema still enters the population and still forces a
disposition.
Also from round 7: `prefixItems` was the one keyword whose `required` handling no
case pinned (its only case carried none), so discarding it stayed green; it has a
case now. And the assumptions docstring is back to describing two assertions
rather than three, which the separator removal resolved on its own.
REBASED onto
|
||
|
|
161c788a70 |
fix(807): the collision check did not walk the tree it was checking
Third round of the same shape in three consecutive commits, and the cleanest statement of it yet. The assertion added one commit ago — no property name may contain the separator — iterated only the top-level `properties` of each named component schema. The resolver it protects walks the WHOLE tree. So the two disagreed exactly where it matters: a dotted name on a nested inline object, or inside an inline request body, was undetected, and those are precisely the places the resolver qualifies a name and can therefore collide. A guard for a hole, with the same hole. `_property_names_containing_the_separator` now recurses over the parsed document the way `_schema_refs` does. Witnessed across five shapes — top-level on a named schema, a nested inline object, an `allOf` arm, under `items`, and inside an inline request body — all detected; the middle three were not, before. The transferable rule, which this issue has now paid for three times in a row: a check must walk the SAME tree as the thing it checks. Not a similar tree, not the obvious subset — the same one. Each of these three defects was introduced by the commit fixing the previous one, so the pattern is not carelessness about the subject, it is that a fix written to close one shape gets tested against that shape only. 908 passed 2 skipped, ruff check 0, ruff format 0. |
||
|
|
021f3a4bb9 |
fix(807): the dotted path I just added could MASK a nested member
Self-attack on the previous commit's new code, in the function every round has found defective — including, twice now, in the block that fixed the last defect. Qualifying a nested inline object's members as `parent.child` assumes no property name contains a dot. JSON permits one. Measured: a schema with a literal `a.b` member alongside a nested `a` -> `b` collides, and when the literal is `required` the nested optional member is MASKED — it vanishes from the population entirely. Silent-miss direction again, introduced by the fix for a silent miss. Zero dotted property names in the document today, which is exactly the reasoning that has been wrong five times on this issue, so the assumption is asserted rather than relied on. The separator is a named constant, the assumptions test now carries a third clause, and the failure message says what to change instead of only reporting that a fact moved. Witnessed: planting a schema with an `a.b` member fires it. 908 passed 2 skipped, ruff format 0. |
||
|
|
4d7fd26a3e |
fix(807): then/else are ALTERNATIVES — stop unioning their required
Round 6 returned BLOCKED on a polarity error I committed in the block that fixed the previous polarity error. `then` and `else` were in the conjunctive list with `allOf`, so their `required` unioned. They are MUTUALLY EXCLUSIVE: a member required only under `then` is omittable whenever `if` does not match. The guard reported it as required — the silent-miss direction, which is the one this file exists to catch. Twelve lines below the defect the same function warns, about `oneOf`, that getting exactly this backwards "is not a detail". The comment made it worse, not better: it asserted the semantics had been considered, which is what stops the next reader checking. Its reasoning was right for properties (union them — a client may send them) and silently inverted for `required`. `if`, `then`, `else` and `dependentSchemas` now share one loop with one rule: collect properties, discard `required`. They discard for three different reasons — a branch selector, a branch that may not be taken, a trigger key that may be absent — but the rule is identical, so one loop beats three arguments. Why fix a keyword the emitter has never produced: this head added `prefixItems` and `dependentSchemas` handling on the stated grounds that "it does not appear in today's document" has been wrong repeatedly here. That standard cannot apply to three keywords and not the fourth — and a wrongly-handled keyword is worse than an unhandled one, because it ships a comment claiming it was thought about. The real lesson is narrower than "handle more keywords": for EVERY keyword whose `required` is unioned or discarded, ship a case that puts a `required` under it. None of the 27 cases did, which is why both this and the unpinned `if` discard were invisible. Three such cases added. Also closes the last blind spot with a mechanism behind it: an inline object under `properties`. The generator recurses into it and emits `"b"?:`, so it is genuinely droppable; the resolver returned nothing. Now reported with a DOTTED path, so `logo.contentType` cannot be mistaken for a top-level `contentType` in a MISSING diagnostic. `patternProperties` is correctly unhandled (no fixed members, like `additionalProperties`) and now has a case saying so rather than being silently absent. The docstring now names the resolver's exact reach instead of gesturing at it, and the `$ref` skip in the inline walk is marked as belt-and-braces rather than reading as protective — deleting it changes no result. Population unchanged at 9, set equality holds. 33 resolver cases, 908 passed 2 skipped, ruff 0/0, typecheck 0, eslint 0, 1200/1200 web, build 0, catalog clean. |
||
|
|
4ce590662a |
docs(807): record what happens on a cyclic schema, measured both ways
The resolver has no cycle guard and that is deliberate, but the reasoning was
nowhere, so the next reader would either add a redundant guard or wonder.
Measured 2026-08-23:
- `_properties_and_required` raises `RecursionError` on a dict that contains
itself by object identity. That input cannot come from `json.load` — JSON has
no back-references — and the failure is a loud red rather than a silent wrong
answer, so it needs no defence.
- The cycle a real document CAN express is a mutual `$ref`. `_request_reachable`
handles it: A -> B -> A resolves to {A, B} and terminates, because of its
`seen` set. This resolver never follows `$ref` at all, which is what makes the
first case the only one available.
Stated where the recursion is, since "why is there no cycle guard here" is
answerable only by knowing both halves.
|
||
|
|
aa24e57343 |
fix(807): the last unannotated item builder, and three more resolver blind spots
Self-attack ahead of round 6, on the two things every round has found defective: the newest hand-written code, and the completeness of an enumeration. CONSTRUCTION SITES, enumerated rather than sampled. 15 wrappers now take `Complete<>`; walking each one's nested item type to its builder leaves exactly one unannotated — `DecosScreen.breakToRequest` / `toReplaceRequest`, feeding the `Complete<>`-annotated `replaceDeco`. Round 5 called this non-blocking because a declared return type is already a contextual position, so the phantom direction fires there. True, but the MISSING direction does not: a new optional member on `DecoBreakContentRequest` would have been droppable. Both annotated. RESOLVER, probed with constructed schemas rather than read. Three more shapes it could not see, all silent misses: items in the 3.0 TUPLE form (a list) -> nothing prefixItems (the 2020-12 spelling) -> nothing dependentSchemas -> nothing `isinstance(items, dict)` skipped the list form silently. All three are handled now; `dependentSchemas` gets the `if` treatment — properties collected because a client may send them, conditional `required` discarded because it binds only when the trigger key is present. Two shapes probed and confirmed already CORRECT, pinned so they stay that way: `additionalProperties` contributes no members (it describes values under arbitrary keys, not named members), and `not` cannot make a member required. `else` was already handled; it now has a case of its own rather than riding on `then`'s. Seven new parametrised cases, 27 in the file. Population unchanged at 9 with set equality — every one of these is a latent hole, not a live miss, which is the point: "it does not appear in today's document" is the reasoning that has been wrong five times on this issue. 902 passed 2 skipped (up from 895), ruff format 0, typecheck 0, eslint 0, 1200/1200 web, build 0. |
||
|
|
d194091536 |
fix(807): close the phantom gap at two sites, pin every resolver branch, stop
claiming coverage in prose Round 5 returned BLOCKED. Its blocker was a durable coverage claim the tree contradicted, and its other findings were the same class one layer down. F1, the blocker. The record said `Complete<T>` "is applied … at each construction site, which is what keeps the phantom direction alive." This PR annotated `replacePathReplacements` and `replaceRemoteLibraryPreferences` at the wrapper but left their construction sites as bare generic `.map` callbacks — the exact shape spa-conventions §4b rule 2 says is unchecked. Injecting `phantomFieldZZZ` into both literals typechecked CLEAN. Both sites are annotated now and both reject the phantom with TS2353. F2. Five mutations of the composition resolver left the whole file GREEN: dropping `anyOf` entirely, making the alternatives loop non-recursive, taking only the first arm's properties, ignoring top-level `required` when alternatives are present, and dropping `items` descent. Three of those are silent-miss direction — a droppable member the guard reports nothing about. The six existing cases pinned the `allOf` path and the `oneOf` intersection and nothing else, so the previous commit's "witnessed" claim covered one branch of five. All five now redden; fourteen parametrised cases instead of six. F3. "A new optional member anywhere in the request graph fails this test" was false in three constructed shapes. Two are now handled — an inline body that is an ARRAY of objects, and properties under `if`/`then` — and the third was a real collision: `_inline_body_members` keyed on verb+path with no media type, so a second `[Consumes]` on one action made the later body silently overwrite the earlier. Keyed by media type now. The universal is replaced in both copies by the actual boundary. `if` is treated as a condition, not a constraint: its properties are collected (a client may send them) and its `required` is discarded, because that `required` selects a branch rather than obliging anyone to send anything. F4/F5. The record characterised the annotated wrappers as "the ones reachable from a droppable schema, plus the six builders" — five fall outside both. And `oneOf` and `anyOf` were concatenated into one alternatives list, which intersects `required` ACROSS the two keywords; they are conjunctive with each other, so each intersects within itself and the results union. Safe-direction but wrong, and untested. The prose change is the one that matters. Five rounds, five blockers, every one a hand-written claim about a population. So the record no longer asserts present coverage at all: it states the RULE, says plainly that nothing enforces the second half (#820), and tells the reader to read the code rather than a summary. A set phrased in words rots exactly like a count — deleting the counts in an earlier commit while leaving the worded set was treating the symptom. 895 passed 2 skipped (up from 887), ruff format 0, ruff check 0, typecheck 0, eslint 0, 1200/1200 web, build 0, catalog clean. |
||
|
|
3758cb9112 |
test(807): pin that the component walk and the inline walk COMPOSE
`_properties_and_required` does not resolve `$ref`, so an `allOf` arm that is a `$ref` contributes nothing to the inline key. I flagged that as a suspected hole and it is not one: the referenced schema is a named component, so `_request_reachable` seeds on it (`_schema_refs` finds a `$ref` anywhere, arms included) and `_optional_members` walks it, and its optional members surface under their own key. Verified by construction — a probe schema behind a `$ref` arm reports `['probeOptional']` under `RefArmProbeSchema` while the sibling inline arm reports `['probeInline']`. Pinned as a test rather than left as a comment for two reasons. Resolving refs in this resolver as well is the obvious "fix" and would report the same member under two keys, which is worse than either. And a reader checking only one of the two walks would reasonably conclude the case is uncovered and go build that fix. 887 passed, 2 skipped. ruff format 0. |
||
|
|
9445330cb6 |
fix(807): resolve schema composition recursively, and by its actual semantics
Self-attack on the inline-body walk added one commit ago, before round 5 reported. Probing it with constructed schemas rather than reading it found two defects, and the second is the more serious: nested allOf inside allOf -> reported NO properties (one-level handling) oneOf, x required in ONE arm -> reported x as REQUIRED The second is backwards, not merely incomplete. `allOf` arms all apply, so their `required` sets union. `oneOf`/`anyOf` arms are ALTERNATIVES, so a member is genuinely required only when EVERY arm requires it — the sets intersect. Unioning them marks a member required because one arm requires it, which hides the arm that lets a client omit it. That is exactly the drop this guard exists to catch, so the resolver was capable of concealing its own subject matter. `_properties_and_required` now recurses and distinguishes conjunction from disjunction, and `_optional_members` uses it too, so named component schemas and inline bodies resolve composition identically instead of the named path silently keeping the old flat behaviour. Effect on today's document: none. Population stays 9, set equality holds. The document exercises exactly one composition shape (a two-arm `allOf` in the artwork upload body), which is precisely why every other branch was unexercised prose — a resolver whose correctness rested on a case the corpus does not contain. So the six probe cases are now parametrised tests, pinned against constructed schemas rather than against today's API. Witnessed: reverting the allOf recursion reddens the nested case. 886 passed, 2 skipped (up from 880), ruff format 0, ruff check 0, catalog clean. |
||
|
|
5371b664e0 |
fix(807): cover inline request bodies; drop three unverifiable counts
Round 4 returned BLOCKED. Its blocker is R2's failure mode repeated: a boundary drawn by hand, stated as a universal, missing a case already present in the document. The guard's docstring claimed "a new optional member ANYWHERE in the request graph" fails the test. It did not. `_optional_members` iterates `components.schemas` and `_request_reachable` seeds from `$ref`s, so a request body declared INLINE is invisible in both directions — and `POST /api/v1/artwork/uploads` declares one, whose `file` and `target` sit outside any `required` array and had no disposition. Planting a third optional member into that body left the guard GREEN. Rather than narrow the claim, `_inline_body_members` now walks inline bodies and keys them `"<VERB> <path> (inline body)"` — there is no schema name to use, which is precisely why the component walk could not see them. Composition arms are unioned, because this body splits its properties across `allOf` and would otherwise report none. The population goes 8 -> 9 and the uploads endpoint acquires a TRIGGER disposition. The planted-member proof now plants into the inline body too: deleting the inline branch reddens two tests, where before the branch could have been deleted wholesale with everything green — the same gap `oneOf` was in one round ago. Live impact of the hole was nil (`artwork.ts` builds that body as hand-rolled FormData, no generated type, create-not-replace), which is why it is worth fixing rather than shrugging at: it was invisible, not harmless. `createFFmpegProfile` was dispositioned COVERED while its wrapper was unannotated, contradicting the vocabulary's own words and spa-conventions §4b rule 1. Annotated; coverage no longer rests on the single `Draft` alias. Three hand-written counts removed rather than corrected. "The eleven schemas the six named builders target" is unverifiable — no natural cut reproduces eleven. "The ten construction sites" has no derived definition of a construction site and was already wrong once on this branch. And the record offered `rg 'Complete<' web/src/api` as a reproduction for "most wrappers do not carry it" — a command that counts the helper and its own test file and cannot produce the claim. A command that does not reproduce is worse than none: it reads as checked. NITs: inventory row moved into alphabetical position; the manifest `why` now says the rename fires BOTH directions, since `expect` names only the MISSING half; two over-120-char comment lines wrapped. Filed #822 for the `test_hook_fire_log` race surfaced while running the suite — its oracle is the shared production hook-fire directory, so any concurrent session reddens it with a message that blames the suite. Verification: ruff format 0, ruff check 0, 880 passed 2 skipped, typecheck 0, eslint 0, 1200/1200 web tests, build 0, catalog clean, doc-narrative 0 new. refs #822 Decisions-Edit: yes |
||
|
|
c31c3cc556 |
fix(807): ruff format, and re-pin case 4 to a member that discriminates
Round 3 returned BLOCKED on a CI-red I would have shipped. BLOCKER — `ruff format --check` reds on the new guard file. Two implicitly concatenated strings fit in 120 chars when joined. `ruff check` was green, which is why this survived; the format step is a separate gate and the merge gate reads COMBINED status, so an advisory-looking red still blocks. The real defect is that ruff was absent from my local gate for six commits while I was reporting "verification: ..." lines that read as complete. Both ruff steps are now run, in the CI's own `git ls-files` form under `bash -c` (zsh has no `mapfile`). FALSE NEGATIVE, third of its kind on this branch — the guard's own comment said "No real request type here can discriminate today, because none has an optional member". That was false when written, and this PR's own derived guard lists eight schemas that contradict it. It also cost the proof its most valuable case. Case 4 is re-pinned from `ReplaceDecoTemplateRequest.name` (already REQUIRED, so it errored with or without `Complete<T>`) to `MultiCollectionItemRequest.weight`, which is genuinely optional and is the exact field whose loss was live before #807. Measured: case 4 now reddens under ALL THREE weakenings including the realistic `{ [K in keyof T]: T[K] }`, where before it reddened only under `Partial<T>`. The table is updated to what was measured, and the file now says that the set of schemas with optional members is DERIVED and must not be restated in prose — three hand-written versions of that list have now been wrong. STALE AFTER THE VERB WIDENING — the MISSING diagnostic still told the reader "reachable from a POST/PUT/PATCH body" one commit after the walk started seeding from every verb, so it could name a DELETE-reached schema while misdescribing where it came from; `expect` matched the tail of the message, so no test caught it. Same sentence fixed in the decision record. `oneOf` COVERAGE WAS CLAIMED, NOT HELD — `_schema_refs` has a dict branch and a list branch, and only the dict one was exercised: deleting list descent (making all 23 `oneOf` refs invisible) left the file GREEN. The planted fixture now goes in through a `oneOf` list, and deleting that branch reddens it. Also: deterministic host selection in the mutation test (set iteration over strings varies per process, so the proof's depth varied run to run); the `#/components/` bucket regex no longer assumes alphabetic bucket names; the hand-maintained "thirteen of thirty-three wrappers" count is replaced by its reproduction command; the record now reconciles its own pin-vs-floor argument against this guard's floors; 141 -> 142; generator path corrected to `web/scripts/`. Verification: ruff format 0, ruff check 0, 880 passed 2 skipped, typecheck 0, eslint 0, 1200/1200 web tests, build 0, catalog clean, doc-narrative 0 new (9 pre-existing warnings, none in files this branch touches). Decisions-Edit: yes |
||
|
|
f878831e8f |
test(807): assert the two document properties the walk silently depends on
`testing.guard-derives-population-from-source`: when a guard's scope mirrors an authoritative source, the mirror needs its own check or it goes stale silently. `_request_reachable` mirrors two properties of the OpenAPI document, both true today and guaranteed by nothing: 1. Request bodies are declared inline on the operation. If ASP.NET emits a `components.requestBodies` bucket and operations `$ref` into it, the seed walk finds no `#/components/schemas/` name at all and every schema behind that shared body drops out of the population — silently, with the guard still green. 2. Every `$ref` points into `#/components/schemas/`. `_schema_refs` matches that prefix, so a ref into any other bucket is invisible to it. Both are now asserted, with a failure message that says what to fix rather than just that a fact changed. Neither was asserted anywhere else, so a change in the emitter would have shrunk this guard's reach without failing anything — the "complete within a scope that has silently gone stale" case. 880 passed, 2 skipped. |
||
|
|
eff4bdfa22 |
test(807): state what the anti-vacuity floors do NOT catch
Probed the new guard against a partially-broken derivation rather than only the total-failure case the floors were written for: deleting the transitive step from `_request_reachable`, so only schemas named directly on a request body resolve. Both floors stay satisfied and `test_the_derivation_reached_a_real_population` stays GREEN. What reddens is the planted-member test — its plant is reached through a nested `$ref` exactly so that it can — and the disposition test, since the nested rows vanish and report as PHANTOM. So the floors are the crude backstop against a parse that reached nothing at all, and the planted-member test is what actually holds the walk honest. Recorded in the docstring, because a floor described as anti-vacuity reads as coverage it does not have, and the next reader deciding which of these to keep working should know which one is load-bearing. |
||
|
|
35e5d42ad2 |
fix(807): drop the verb allow-list from the derivation
Self-review of the new guard, before round 3 reported: `_request_reachable` seeded its walk from POST/PUT/PATCH request bodies only. That reads as obviously right and already had a counterexample in the document — `DELETE /api/v1/media-items` carries a request body. A DELETE body probably cannot cause a full-replace drop. "Probably" is what produced both of this issue's earlier misses, and a verb allow-list is another hand-drawn edge on a population that has now been drawn wrongly twice. Every operation carrying a request body now seeds the walk, so anything it surfaces must acquire a stated disposition rather than being argued away in advance. Effect measured: reachable schemas 141 -> 142, droppable set unchanged at 8, set equality against the registry still holds. So this buys no new rows today; it removes a place to be wrong later. Also states in the docstring why the walk is transitive and why `_schema_refs` recurses over every dict value and list element rather than naming composition keywords — `oneOf` appears 23 times in the document today, and naming keywords is the same frozen-filter mistake one level down. 879 passed, 2 skipped. |
||
|
|
685926a0d0 |
fix(807): derive the coverage boundary instead of writing it down a third time
Round 2 returned BLOCKED. The enumeration added in the previous commit was
itself incomplete: it omitted `ArtworkContentTypeModel`, which carries three
members outside its `required` array and is reachable from the full-replace
`PUT /channels/{id}` via `UpdateChannelRequest.logo`. It was sorted out on its
name — `...Model` reads as a response model — which is the same failure the
enumeration was written to eliminate one level up, and it closed with a false
universal negative ("the remaining schemas ... are never request bodies") sixty
lines below this document's own warning against exactly that.
Two misses from one mechanism, so the mechanism goes rather than the list
getting a third patch.
`scripts/tests/test_optional_request_members.py` derives the population every
run from `ErsatzTV/wwwroot/openapi/v1.json`: every schema with a property
outside `required` that is TRANSITIVELY reachable from a POST/PUT/PATCH request
body, asserted set-equal in both directions against a registry of per-schema
dispositions. Transitivity is load-bearing — `MultiCollectionItemRequest` and
`ArtworkContentTypeModel` are both nested, so a top-level-only check would have
reproduced both misses. Population derived, dispositions hand-written as the
reviewed SCOPE, per testing.guard-derives-population-from-source.
Witnessed red: removing the `ArtworkContentTypeModel` disposition reports it as
MISSING with its three members. That mutation is DECLARED in
`mutation_manifest.py` and re-applied every suite, so the row grades MUTATION
rather than resting on this message. Renaming the key rather than deleting the
entry keeps the module importable, so the red is the MISSING direction and not
an ImportError reddening for the wrong reason.
ArtworkContentTypeModel's real disposition is COMPUTED, not COVERED: the three
members are get-only properties derived from `Path` and are never deserialized,
so omitting them drops nothing — and `Complete<T>` must NOT be applied there,
because it would force a caller to fabricate server-computed values in an
outbound request. That caveat is now in spa-conventions §4b, where a reader
following "annotate each construction site" would otherwise have produced a
wrong payload.
Also reconciles the contradiction round 2 found between the wrapper-boundary
rationale and the snapshot: 13 of ~33 full-replace PUT wrappers carry
`Complete<>` today. The rest are stated as still contingent — the difference
from before is that the contingency is now MONITORED by the derived test rather
than assumed.
L5 the phantom denominator is ten sites, not nine (five accepted, three of those
without a spread or a local). N6 fixture field placed alphabetically. N7 the
record's three-act framing trimmed to the current statement plus the rejected
one, so a cold reader does not have to work out which is live.
Verification: 879 passed 2 skipped in scripts/tests (up from 874), typecheck 0,
eslint 0, 1200/1200 web tests, web build 0, catalog check clean, doc-narrative 0.
Decisions-Edit: yes
|