test(lint): pin getNested's array fan-out to a synthetic warn map - #10399
Conversation
…0262) The fan-out — a dotted warn-map path resolved over an ARRAY container level must visit EVERY element, not just index 0 — is reachable only from a DOTTED warned entry, because `checkItem` takes the `path.includes('.') ? getNested(item, path) : [item[path]]` branch. Its subject was therefore always a ledger VERDICT, and verdicts move: twice a row correctly flipping to `live` deleted the only test of the walk (#6774 → #7079, then #10068 → #10262). Measured across all 30 shipped ledgers, every remaining warned entry is top-level, so there is nothing left to re-subject to. Adds a package-internal test seam — `getNested` and `checkItemAgainstWarnMap` exported from the MODULE only, neither re-exported by `src/index.ts` nor reachable through the package's `exports` map — and ten assertions driving it with a synthetic warn map, including an `it.each` that authors the warned key on exactly one entry of a four-entry container so a walk stopping at index 0 fails three of four. Every other assertion in the file stays ledger-driven, and the #10068 silence pin plus its anti-vacuity guard are byte-identical (the only line removed from the test file is the import). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gcKVsiywU9CcS96S5t9qD
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c623bf07d7a909523e7f870a574537d57236942c && git checkout c623bf07d7a909523e7f870a574537d57236942c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b9a5c46dc964a587a99ee508560080222ec9b4ee 5d98d1efb0b8691eaca677dee468dceadb6f7ed6 && git checkout -B drift-repro b9a5c46dc964a587a99ee508560080222ec9b4ee && git merge --no-ff 5d98d1efb0b8691eaca677dee468dceadb6f7ed6
node scripts/docs-audit/affected-docs.mjs --json b9a5c46dc964a587a99ee508560080222ec9b4ee |
Fixes #10262
Restores direct coverage of
getNested's array fan-out — the property that a dotted warn-map path resolved over an array container level must visit every element, not just index 0.Why this one assertion leaves the ledger
The fan-out is reachable only from a dotted warned entry, because
checkItemtakes thepath.includes('.') ? getNested(item, path) : [item[path]]branch. Its subject was therefore always "whichever ledger row happens to carryauthorWarnunder an array container today" — and a ledger verdict is supposed to move. Twice a row correctly flipping tolivesilently deleted the only test of the walk:dashboard.widgets.colorVariantwent liveapp.…navigation.children.runActionnavigation.runActionwent liveMeasured across all 30 shipped ledgers, every remaining warned entry is now top-level, so there is nothing left to re-subject to and no reason to expect a third subject to last. A broken walk is invisible without direct coverage: a
getNestedthat stopped at index 0 still warns on every single-entry fixture, on every top-level warned key, and on the first item of every real app.The seam — no new public export
getNestedand a newcheckItemAgainstWarnMapwrapper are exported from the module only:packages/lint/src/index.ts(which exports exactlylintLivenessProperties, theLivenessLintFindingtype, and the two rule-id constants);@objectstack/lint'sexportsmap publishes exactly two subpaths,.→dist/index.jsand./runtime→dist/runtime.js, both bundled by tsup from those two entries — so neither symbol is reachable by any consumer and the built.d.tssurface is unchanged.The test imports
./lint-liveness-properties.jsdirectly, which is how every other test in this package already reaches its subject.LedgerEntryis exported as a type for the seam's signature, on the same module-only footing. This adds nothing to the published API — flagged here because the dispatch asked for the seam design to be judged.Scope held
Every other assertion in the test file stays ledger-driven — those are genuinely contract tests about the ledger. The #10068 silence pin and its anti-vacuity guard are byte-identical: the only line removed from the test file is the import statement (
git diffshows exactly one-line).Verification
Gate union re-derived from the actual diff with
node scripts/pm/dispatch-gates.mjs(no paths — the script takes the change set from the merge base) and run at commit5d98d1efb:pnpm --filter @objectstack/lint typecheckcommand-exit 0@objectstack/lintfull suitesrc/lint-liveness-properties.test.tscheck:cross-package-test-inputscheck:query-options-erasure0bfbeb7check:type-check-coveragecheck:type-check-debt(--re-measure, full closure built first)check:engine-double-contractcheck:where-matchercheck:nul-bytesscripts/docs-audit/check-affected-docs.mjscheck:slot-lookupcheck:slot-lookupfirst failed withpackages/spec/src/migrations/registry.ts:0:0 — Parsing error: Maximum call stack size exceeded. That file is byte-identical toorigin/mainand untouched by this PR (git diff origin/main -- packages/spec/src/migrations/registry.tsis empty); it is the known parser cliff tracked by open issue #10122, whose documented intermittency reproduced exactly — an unchanged retry passed:✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new. Not this PR's condition, and no duplicate filed.Reverse verification
The new assertions were proved to fail on the documented broken-walk shape.
for (const el of c)→for (const el of c.slice(0, 1))ingetNested, confirmed on disk before running (injected text count 1, original text count 0 — not inferred from an editor exit code). No rebuild was involved: the test imports the TypeScript source through a relative specifier in the same package, so nodist/sits between the mutation and the run.Predicted direction — the new block goes red while every ledger-driven assertion stays green, because no dotted warned entry remains for them to exercise. Observed exactly that: 6 failed | 37 passed, all six failures inside the
#10262block, includingand the
navigation[1]/[2]/[3]cases of theit.each(case[0]passing is the asymmetry that makes a single-entry fixture a non-test). Restored from the committed state withgit checkoutof the branch's copy of the file, proved absent on disk (injected count 0, original count 1), and re-run: 43 passed, identical to the pre-mutation run.Changeset
None —
skip-changeset. Judged from the actual diff: it is one test file plus a source file whose only change is marking two existing functions and one interfaceexportat module scope. Nothing reaches a published entry point, sodist/index.js,dist/runtime.jsand both.d.tssurfaces are unchanged and no consumer-visible behaviour moves. Per AGENTS.md §943 a changeset covers feature work, and.github/workflows/lint.ymlstates the prescription for this shape directly: "such a PR releases nothing, so by the workflow's own prescription it takes the label."Generated by Claude Code