Skip to content

test(lint): pin getNested's array fan-out to a synthetic warn map - #10399

Merged
qq9340100 merged 1 commit into
mainfrom
claude/issue-10262-getnested-walker-seam
Aug 20, 2026
Merged

test(lint): pin getNested's array fan-out to a synthetic warn map#10399
qq9340100 merged 1 commit into
mainfrom
claude/issue-10262-getnested-walker-seam

Conversation

@qq9340100

@qq9340100 qq9340100 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

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 checkItem takes the path.includes('.') ? getNested(item, path) : [item[path]] branch. Its subject was therefore always "whichever ledger row happens to carry authorWarn under an array container today" — and a ledger verdict is supposed to move. Twice a row correctly flipping to live silently deleted the only test of the walk:

flip subject lost filed as
#6774dashboard.widgets.colorVariant went live the fan-out's original subject #7079
#7079 was then closed by re-subjecting to app.…navigation.children.runAction
#10068navigation.runAction went live the replacement subject #10262

Measured 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 getNested that 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

getNested and a new checkItemAgainstWarnMap wrapper are exported from the module only:

  • neither is re-exported by packages/lint/src/index.ts (which exports exactly lintLivenessProperties, the LivenessLintFinding type, and the two rule-id constants);
  • @objectstack/lint's exports map publishes exactly two subpaths, .dist/index.js and ./runtimedist/runtime.js, both bundled by tsup from those two entries — so neither symbol is reachable by any consumer and the built .d.ts surface is unchanged.

The test imports ./lint-liveness-properties.js directly, which is how every other test in this package already reaches its subject. LedgerEntry is 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 diff shows 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 commit 5d98d1efb:

gate result
pnpm --filter @objectstack/lint typecheck command-exit 0
@objectstack/lint full suite 77 files, 2142 passed / 5 skipped
src/lint-liveness-properties.test.ts 43 passed (33 pre-existing + 10 new)
check:cross-package-test-inputs OK — 12 packages read outside themselves, all declared
check:query-options-erasure ratchet holds, no files added vs 0bfbeb7
check:type-check-coverage OK — 64/77 type-checked
check:type-check-debt (--re-measure, full closure built first) OK — 33 entries re-measured, none above its recorded number
check:engine-double-contract OK — 338 pinned
check:where-matcher 266 matchers, 0 silently-wrong, none new
check:nul-bytes OK — 6115 files, no raw control bytes
scripts/docs-audit/check-affected-docs.mjs exit 0
check:slot-lookup red on first run, green on retry — see below

check:slot-lookup first failed with packages/spec/src/migrations/registry.ts:0:0 — Parsing error: Maximum call stack size exceeded. That file is byte-identical to origin/main and untouched by this PR (git diff origin/main -- packages/spec/src/migrations/registry.ts is 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)) in getNested, 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 no dist/ 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 #10262 block, including

AssertionError: expected [ 'create_0' ] to deeply equal [ 'create_0', 'create_1', 'create_2' ]

and the navigation[1] / [2] / [3] cases of the it.each (case [0] passing is the asymmetry that makes a single-entry fixture a non-test). Restored from the committed state with git checkout of 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 interface export at module scope. Nothing reaches a published entry point, so dist/index.js, dist/runtime.js and both .d.ts surfaces are unchanged and no consumer-visible behaviour moves. Per AGENTS.md §943 a changeset covers feature work, and .github/workflows/lint.yml states 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

…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
@qq9340100 qq9340100 added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 20, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b9a5c46dc964a587a99ee508560080222ec9b4eepackageMentionDocs.

Which tree this was computed on

This run read content/docs from c623bf07d7a909523e7f870a574537d57236942c — the merge of head 5d98d1efb0b8691eaca677dee468dceadb6f7ed6 into base b9a5c46dc964a587a99ee508560080222ec9b4ee, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@qq9340100
qq9340100 marked this pull request as ready for review August 20, 2026 18:15
@qq9340100
qq9340100 enabled auto-merge August 20, 2026 18:15
@qq9340100
qq9340100 added this pull request to the merge queue Aug 20, 2026
Merged via the queue into main with commit 2aca1bc Aug 20, 2026
34 checks passed
@qq9340100
qq9340100 deleted the claude/issue-10262-getnested-walker-seam branch August 20, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants