The SPA page-size guard enumerates web/src with a Vite glob while claiming completeness
#819
Closed
opened 2026-08-22 17:38:54 +02:00 by timothy
·
2 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#819
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.
Found by the cold review of #806 (PR #818), by sweeping the whole repo rather than the file list #806 named — the same method that turned up
test_pr_changed_files.pyinside that PR.The defect
web/src/api/pageSizeCallSites.guard.test.ts:352derives its population from the filesystem:and then asserts exact completeness against
REGISTRY— a newly discovered, unregistered site fails, which is the whole point of the guard (#650 could otherwise recur invisibly).That is
testing.guard-derives-population-from-source's file-population case, which #806 instantiated: a directory enumeration is not an authoritative source. An untracked.ts/.tsxunderweb/src/containing a page-size call — a scratch file, a half-finished component, an editor dropping — enters the population and fails as unregistered on that checkout, while CI never sees it. Developer-red / CI-green, the #778 shape.The direction is the safe one (it over-enumerates, so it cannot go silently blind), but it is the shape #806 exists to remove.
Why it was not fixed in #818
The glob is not incidental — it is a documented, deliberately-chosen workaround, and the obvious fix runs straight into a prior reverted attempt. From the comment above it:
So filtering the glob's result against
git ls-filesneedsnode:child_processin a project that deliberately excludes Node types, and the last attempt to admit them broke three unrelated tests. That is a real piece of work with a known obstacle, not a one-line conversion, and #818's worktree cannot even run the web suite.Shape of a fix
The glob is resolved by Vite at transform time and cannot consult git. The filter can, though, if it runs somewhere with Node available:
import.meta.globas the discovery mechanism, and intersect its keys with a tracked-file list obtained at test time — either through a small Vite plugin /definethat injectsgit ls-filesoutput at config time (where Node IS available), or a vitestglobalSetup, both of which sidestep thetsconfig.app.jsonproblem entirely;Either is acceptable; silence is not, which is the rule #774 and #806 both land on.
Related
docs/guard-inventory.mdthat now carries this as a deferred rowdocs/guard-inventory.mdscope-limit item 2 already records that C# and TypeScript guards sit outside the inventory population, so this guard has no row and noMUTATIONgrade eitherErsatzTV.Mcp.Tests/ToolCatalogTests.csis the other guard in that class; it derives from the generated OpenAPI document rather than a directory walk, so it is not affectedDone-when
pageSizeCallSites.guard.test.ts's population is derived from the git index, or the residual is stated in the guard with its reasondocs/guard-inventory.md's deferred row updated or removedClaiming this (Claude Code session, 2026-08-28). Pre-claim checks all clear: no open PR references #819,
git ls-remote --heads origin '*819*'is empty, no prior comments on the issue, andorigin/mainfetched fresh ate11d57719.Plan: keep
import.meta.globas the discovery mechanism and intersect its keys with agit ls-files-derived tracked set obtained where Node IS available (vitestglobalSetup/ a config-time seam), sidestepping thetsconfig.app.jsonNode-types obstacle entirely. Will also update the deferred row indocs/guard-inventory.md.Not bundling #820 — it is the same defect class but requires a whole new compiler-API guard, which is its own session.
Closing record
Outcome: Shipped in PR #875.
web/src/api/pageSizeCallSites.guard.test.tsnow derives its filepopulation from the git index instead of
import.meta.glob, via a new Vite plugin(
web/vite-plugins/trackedSourceFiles.ts) that reaches git in Vite's own Node context and hands theresult to the app project as a virtual module — which is how the index is reached without admitting
@types/nodetotsconfig.app.json, the obstacle that deferred this in #818.Root cause: The guard asserted EXACT completeness over a population enumerated by a directory
walk. A walk answers a question about the machine, not about the repo, so an untracked
.ts/.tsxunder
web/src/entered the population and failed as unregistered on that developer's checkout whileCI — which only ever checks out tracked files — stayed green.
Decisions/conventions changed:
testing.guard-derives-population-from-sourcegains twoadditions (catalog regenerated):
OTHER direction reports the on-disk hole, so the absent member stays representable;
predicate with no external source, the restatement proving it must share no helper, at any
depth, with the predicate it checks, and the population it reads must be cross-checked against an
independently derived list.
Reusable knowledge: A proof that shares anything with its subject proves nothing about the
shared part. Sixteen review rounds all found one mechanism — the shared thing sits on both sides of
the comparison and cancels, so a narrowing shrinks both and passes. It recurred through five distinct
carriers: the scope itself, a table of example paths (4 of 8 directories), a delegated sub-predicate,
a
basenamehelper that read as plumbing rather than policy, and finally the population array everycomparison was derived from. Two of those were blind — they hid a planted call site with the
whole suite green.
Two things ended it. First, stating the criterion as a checkable property rather than an
instruction: "restate the scope" gives no way to tell when you are done; "share no helper at any
depth" does. Second, where a restatement cannot police the population it reads, cross-check against
a genuinely different query (
ls-files --othersbesidels-files) rather than restating harder.A second, sharper lesson: for the last six rounds the only defects were false coverage/direction
claims in prose — sentences asserting that something was caught, pinned, or harmless, written
without measurement, in a change whose entire subject is the difference between a check and the
appearance of one. Four of them were mine. One was actively harmful:
docs/testing.mdtolddevelopers to "restart the watcher", which resolves a noisy red by putting them into a blind green.
Fail-direction is the argument for tolerating a residual, so it must be measured, never inferred.
Third: a "gitless replica" that deletes
.gitbut leaves thegitbinary onPATHreproduces onlyhalf the condition. That gap shipped a Dockerfile line which would have failed
docker buildonevery push to
mainand every release tag.Verification:
npx vitest run119 files / 1272 tests;tsc -b --force;eslint .;npm run build;pytest scripts/tests1228 passed / 2 skipped;decisions_validateOK; catalogzero-diff. Defect witnessed before the fix and each residual's direction measured by planting a real
pageSizecall site. Docker web-build stage replicated with both.gitabsent andgitoffPATH: rc=0, 117 files / 1226 tests. Sixteen cold-context adversarial review rounds in isolatedworktrees; the final two returned MERGEABLE with nothing at any severity.
Deferred: Six residuals, each stated in the guard with its measured direction — (2), (4) and (5)
fail-noisy; (1) a three-site coordinated edit and (3) a union-preserving mispartition are BLIND;
(6) watch-mode staleness is each in turn. (6)'s obvious fix (
configureServerinvalidation) wasimplemented, measured and rejected:
git addfires no watcher event, so it blinds thecreate-then-stage sequence. No follow-up issue — these are properties of the mechanism, recorded
where the next author edits, not open work.
Docs updated:
docs/guard-inventory.md(audit row converted from DEFERRED, scope item, residuallist),
docs/testing.md(the checkout-vs-binary distinction and the watch-mode caveat),docs/spa-conventions.md(§1 gains theweb/vite-plugins/build-time seam),docs/decisions/records/testing/guard-derives-population-from-source.md+ regenerateddocs/decisions/README.md.