Complete<T> proves its semantics but not its APPLICATION — nothing derives the set of SPA full-replace construction sites
#820
Closed
opened 2026-08-22 22:10:58 +02:00 by timothy
·
3 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#820
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Residue from #807, named there rather than implied closed. Filed by the adversarial review of PR for #807, which demonstrated it by execution.
The gap
#807 made SPA full-replace request bodies
Complete<T>(web/src/api/completeRequest.ts) so a builder that omits a schema member failsnpm run typecheck. Its guard (web/src/api/completeRequest.guard.test.ts) proves the TYPE's semantics — thatComplete<T>rejects a missing member and a phantom one.It does not prove the type is APPLIED anywhere. Stripping every
Complete<>fromweb/src/api/blocks.tsandweb/src/screens/BlocksScreen.tsx— i.e. reverting one of the covered screens to its pre-#807 state — leavesnpm run typecheckclean and the guard test green.npm run lintfires only on the now-orphanedimport type { Complete }; a regressor who removes the import too, or a newly added full-replace wrapper that never had one, produces no signal at all.This is
testing.guard-derives-population-from-sourceexactly: the population of construction sites is derived from nothing, so the guard cannot see the site that is missing. #807 found its own two uncovered wrappers by hand-grep, which is the method this repo has recorded as insufficient.Why this is the harder half
Per
testing.guard-derives-population-from-source, a population of sites in code has no external enumerator and is explicitly scoped out of that record (tracked in #777). So this is not a set-equality-against-a-source problem.But the repo already owns a working precedent for exactly this shape:
web/src/api/pageSizeScan.tswalks the SPA with the TypeScript compiler API andpageSizeCallSites.guard.test.tscross-checks the discovered site set against a hand-reviewed registry in both directions (new-and-unregistered vs registered-but-gone), with anti-vacuity floors and an identity deliberately keyed(file, kind, value)rather than line/column. That is the model to copy.Sketch
Discover, via the compiler API, every call to a
web/src/api/*write wrapper whosebodyparameter type is (or should be) a full-replace request, and every object literal flowing into one. Cross-check against a registry recording, per site, whether it isComplete<>-annotated and why if not. ReadpageSizeCallSites.guard.test.ts's doc comment first — it documents its own residual blind spots (spreads, positional args, forwarded expressions), and those apply here too.Worth deciding early whether the cheaper answer is to make the wrapper signatures the ONLY seam (so a site cannot construct a body except through a
Complete<>-typed parameter) rather than scanning for sites at all — dedup by construction beats enumeration, perdocs/defect-shapes-773.md§4 detector C.Done-when
v1.d.ts) and theComplete<…>annotation sites (SPA AST, intersected with the git index). Reworded from "full-replace construction sites (or wrapper signatures)": a wrapper-signature population was built and REMOVED after four review rounds defeated it, and the per-construction-site population is scoped out bytesting.guard-derives-population-from-source(#777). Rationale and the offer to revert this rewording are in the scope-divergence comment belowdocs/guard-inventory.mdif the guard lands in a globbed population, or an explicit note if it stays outside itClaiming this (Claude Code session
main-3, 2026-08-29). Checked before claiming: no remote branch naming 820, no open PR with afixes/refs #820body (the two Renovate PRs matching the string carry it in a version/changelog line, not a reference), no prior comments, freshgit fetch origin mainat8aeacd534.Starting from the question the body raises rather than the sketch: whether the wrapper signatures can be made the ONLY seam, so a body cannot be constructed except through a
Complete<>-typed parameter. Dedup by construction beats enumeration, and it would make the derived-population problem moot rather than solved. If that does not hold for every wrapper, the fallback is thepageSizeScan.tscompiler-API model, both directions with an anti-vacuity floor.Scope divergence on Done-when box 1 — flagging rather than quietly ticking it
Box 1 asks that "the population of full-replace construction sites (or wrapper signatures) is DERIVED, not hand-listed". What shipped derives a third population: the droppable schemas (parsed from the generated
v1.d.ts) and theComplete<…>annotation sites (SPA AST ∩ the git index). Neither of the two the box names is in the final branch. Recording why, because this is a scope reduction and a reviewer should be able to push back on it.Wrapper signatures were built, and removed.
scanWriteWrappersshipped in an intermediate commit and was defeated four separate ways across review rounds:Complete<T>is shallow, so a wrapper annotation never reached a nested request type (confirmed by execution: withComplete<UpdateMultiCollectionRequest>,{ items: [{}] }typechecks clean);body, so renaming it topayloadmade a wrapper vanish from the population — and separately, bodies built as typed locals were invisible;export function→export constrefactor removed real, measured protection while the AST scan and its supposedly independent regex cross-check went blind together, because both keyed on the tokenexport function.Five defects from one mechanism, each found only by review.
process.enumerate-workaround-behaviors-before-deletingand the withdrawntest_review_verdict_vocabulary_parity.py(six rounds, then deleted) both say to remove the mechanism rather than patch a sixth time, and the reviewer who found #4 independently recommended narrowing rather than withdrawing. ~250 lines went.Construction sites are scoped out by an existing decision.
testing.guard-derives-population-from-sourceexplicitly excludes "sites in code" as having no external enumerator; that is tracked in #777. The issue body itself says as much under "Why this is the harder half".So box 1 as written is not satisfiable with the tooling available, and I have reworded it to the delivered scope with this comment as the pointer. If you would rather the box stayed as written and the issue stayed open for the wrapper/site half, say so and I will reopen it — the removal is well-evidenced but it is less than the box asked for.
The other four boxes are met on their own terms: both directions are reported separately with anti-vacuity floors; the witnessed red was measured independently by two reviewers (stripping
Complete<>fromMultiCollectionsScreen.toItemRequest— the exact pre-#807 form — reddens); the guard-inventory carries a row plus the out-of-globbed-population note; and adversarial review is at five rounds.What the guard actually does not cover, stated here because the docs now lead with it: the obligation is per-SCHEMA, not per-site and not per-wrapper, so moving an annotation off an API wrapper onto another production file stays green; it is token presence, so a dead
export type X = Complete<Y>discharges it; and the phantom direction is not checked at all.Closing record
Outcome: Shipped in PR #883 (squashed to
e8f80c42c).web/src/api/completeAnnotationScan.ts(compiler-API scanners) +completeAnnotations.guard.test.ts(the guard) + a synthetic-source fixture suite, plusscripts/tests/test_complete_annotation_dispositions.pycross-checking the SPA disposition table against the authoritative Python one. Two derived populations — theComplete<…>annotations (SPA AST ∩ git index) and the droppable schemas (parsed from the generatedv1.d.ts). Scope was reduced: see the scope-divergence comment above; box 1 was reworded and the offer to revert that rewording stands.Root cause:
Complete<T>proved its own semantics and nothing proved its APPLICATION. Worse,test_optional_request_members.py'sCOVEREDdisposition was worded "the SPA builds this body and the builder is annotatedComplete<T>" — a claim about another language's source that nothing verified, so aCOVEREDrow and a SPA with the annotation deleted were indistinguishable to the entire suite. The same file'sCOMPUTEDprohibition (§4b's "do NOT annotate a schema whose optional members are computed server-side") was prose with no executable form.Decisions/conventions changed: none added.
testing.full-replace-asserts-field-listupdated — itsmechanics:now states which half is machine-checked, and the body's residue paragraph was corrected (it had described the guard as covering the wrapper half, which was measured false).docs/spa-conventions.md§4b now leads with what is NOT checked.Reusable knowledge:
COVEREDwording had been true when written and nothing would have reported the day it stopped being.Complete<T>is SHALLOW, and reachability is not protection.Complete<UpdateMultiCollectionRequest>requiresitemsto be present and requires nothing of each item —{ items: [{}] }typechecks clean. A wrapper annotation never protects a nested request type; only the nested builder's own annotation does. I got this wrong for a full round and the doc sentence justifying it was false.Complete<T>works but must not be applied blanket-wise. Probed against the real generated types: it catches a missing nested member, preserves thenullarm ofnull | Array<X>, keeps explicitundefinedlegal and still rejects phantoms. NOT adopted:UpdateChannelRequest.logoreachesArtworkContentTypeModel, where annotating would force a caller to fabricate server-computed values, and it closes only the missing direction — the phantom direction needs a fresh literal in a contextually typed position, which a generic.mapcallback's return is not. Recorded so it is not re-proposed on plausibility.export function, so anexport function→export constrefactor blinded both at once while I had written that "the two fail in unrelated ways".Complete<>is the #754 mechanism in disguise — the annotation proves the caller filled in the MIRROR.Verification: web 1319 tests green;
scripts/tests1243 green (2 skipped); typecheck + eslint + ruff clean;decisions-validate: OK;build_decisions_catalog --checkup to date;check-doc-narrative --diff0 warnings;npm run check:apiandnpm run buildclean; guard-inventory summary counts machine-checked. CI on68caecbgreen (image-push correctly skipped for a PR). Nine mutations witnessed by hand during development; the Python cross-check carries a DECLARED clause mutation inmutation_manifest.py, executed by the harness every run.Deferred:
scanWriteWrapperswas built and REMOVED after four review rounds each defeated it; the removal rationale is indocs/guard-inventory.mdso the cheap version is not rebuilt without reading why it failed.setupFilesentry could discharge the obligation (three-site coordinated edit). Stated as a residual rather than closed; derivingsetupFilesneeds another virtual-module plugin.web/vite.config.ts:36still describes vitest's default include as**/*.{test,spec}.*; the real 4.1.9 default is**/*.{test,spec}.?(c|m)[jt]s?(x). Pre-existing (from #445), conservative in direction, left alone as outside this change's scope.Docs updated:
docs/spa-conventions.md(§4b),docs/guard-inventory.md(guard row + the out-of-globbed-population note + a row and summary counts for the new Python guard),docs/decisions/records/testing/full-replace-asserts-field-list.md(+ regenerated catalog),scripts/tests/test_optional_request_members.py(the COVERED/COMPUTED wording this guard now verifies).