fix(lint): no authoring rule throws on a non-record entry of any stack collection - #15751
Conversation
…n-record-readers-every-collection
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 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 df86a1e311a21cb59b2591f5cc7fb4bc54aa029b && git checkout df86a1e311a21cb59b2591f5cc7fb4bc54aa029b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 66e68adc667acaca9035c135f57d9f13c8cde56a 4281c5d609e888771b268c1e551e2ffadb18b8bb && git checkout -B drift-repro 66e68adc667acaca9035c135f57d9f13c8cde56a && git merge --no-ff 4281c5d609e888771b268c1e551e2ffadb18b8bb
node scripts/docs-audit/affected-docs.mjs --json 66e68adc667acaca9035c135f57d9f13c8cde56a
|
…n-record-readers-every-collection
#15741 re-pointed `validate-chart-bindings.ts` onto `recordsOf`, so both rows naming it here — the `COPY_LEDGER` entry and the `UNGUARDED_ALLOWANCE` entry — stopped describing the package. Both lists are exact in both directions, so the gate went red on `names no copy the ledger has outlived` and `allows no unchecked branch the allowance has outlived`, each naming the file. That is the mechanism working: the allowance was dated to the change that removes it, and it came out because the test failed rather than because anyone remembered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Same self-expiry, one layer down from the gate. `RESIDUAL_THROWS` recorded that `validateReferenceIntegrity` still threw on a non-record `stack.datasets` member through `indexDatasets`; #15741 re-pointed that reader, so the sweep went red demanding a throw that no longer happens. The rows come out because the assertion failed, which is what "exact in both directions" is for — a residual list that only ever grew would rot into a list of excuses. `objects[].fields` / `validateStackExpressions` (#15742) stays: measured, still throwing on this tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
|
ACCEPT — PR #15751 (head The ordering fix is exactly what was asked, and the mechanism proved itself: with #15741 merged in ( Measured by this seat: Rulings stand: Q2 A; Q1 → #15740 (decision box). Flip + arm when both required jobs report success on Generated by Claude Code |
|
Flipped ready + auto-merge enabled (06:50Z, Generated by Claude Code |
Fixes #15636
23 collection readers in
packages/lint/srccoerced a stack collection with a hand-copiedasArraywhose array branch was an unchecked cast, so a non-record member reached a dereference and threw a stack trace out of a rule that is contractually(stack) => Finding[]. 20 of them now read through the shared guardedrecordsOf; a 21st (validate-chart-bindings.ts) landed as #15741 and this branch merges it; the last two are deferred with dated, self-expiring allowances, and a new source-text test pins that the predicate has one home.Measured, before and after
Every
AUTHORING_RULESentry (42) driven over five non-record shapes per collection, both legs in one worktree: AFTER at head, BEFORE by checking the 20 readers back to1c1421401and re-running (the tests import SOURCE, so no rebuild sits between the legs), then restoring — proven bygit hash-objectequal to the HEAD blob for all 20 and an emptygit diff HEAD.stack.flowslintFlowPatterns,validateFlowTriggerReadiness,validateReferenceIntegritystack.permissionsvalidateCapabilityReferences,validateRlsPredicateEnforceabilityobjects[].fieldsvalidateCapabilityReferencesfixed;validateStackExpressionsremains (#15742)stack.pagesvalidateResponsiveStylesstack.capabilitiesvalidateCapabilityReferencesstack.actionsvalidateCapabilityReferencesstack.hooksvalidateReferenceIntegritystack.datavalidateCapabilityReferencesobjects[].actionsvalidateCapabilityReferencesstack.datasetsvalidateReferenceIntegrityviavalidate-chart-bindings.ts; 1 after THIS change alone, 0 on the branch head since #15741 landed and was merged inDistinct rules that throw on some collection: 7 before, 2 after for this change measured alone. #15741 then closed
stack.datasets, so the branch head stands at 1 —validateStackExpressions, filed as #15742. The card said the per-collection count was unknown rather than zero; this is that measurement.Phantom findings are measured the same way and are unchanged at 0 everywhere except
stack.agentswith an[]member, which draws 1 both before and after: the agent readers filter with!!x && typeof x === 'object', and an array passes that.recordsOfusesisRec, so #15728 closes it.What changed
20 readers re-pointed (
asArraydeleted, call sites ontorecordsOf):build-access-matrix,lint-flow-patterns,lint-liveness-properties,validate-action-locations,validate-action-name-refs,validate-approval-approvers,validate-capability-references,validate-component-props,validate-component-types,validate-dashboard-action-refs,validate-dataset-references,validate-flow-template-paths,validate-flow-trigger-readiness,validate-nav-access,validate-readonly-flow-writes,validate-readonly-hook-writes,validate-responsive-styles,validate-rls-predicate-enforceability,validate-semantic-roles,validate-translation-references.Three of the 23 were not re-pointed here, each with a dated allowance that expires by failing. One of the three has since landed:
validate-chart-bindings.ts— chart-measure-unknown errors on report/list/page chart series[].name with a message that names an empty series — at the pin it is a display-name override that lands on nothing #15575's file; re-pointed there by fix(lint): chart series[].name advises instead of blocking — presentation, not a query binding #15741, now merged into this branch. Its two allowance rows were deleted because this branch's own gate and sweep went red naming it — see the ordering evidence below.validate-object-field-refs.tsandvalidate-list-view-field-refs.ts— re-pointing these is a behaviour change and was reverted to byte-identical-to-main.recordsOfdrops a member, so positional paths renumber, andreference-integrity-suite.test.tsdeliberately pins the opposite:objects[1].highlightFields[1], with the comment "each member's own loop keeps walking the raw array, so reported positions stay stable against the author's file" (indexObjectGraph throws a TypeError on a null entry in stack.objects — every reference-integrity rule crashes on it #15494). Both already guard every member withisRecinside the loop, so neither ever threw — the copy buys them the INDEX, not safety. Filed as finding: two lint readers cannot move ontorecordsOf— dropping a member renumbers the finding paths #15494 pins #15740, which asks whetherobject-graph.tsshould grow an index-preserving companion; that is a shared-surface decision, not a developer's.The one-copy gate —
packages/lint/src/collection-coercion-single-copy.test.ts, a vitest source-text pin inside the package rather than a rootcheck:*script. The family precedent for pinning facts aboutpackages/lint/src's own source is in-package and vitest-shaped (rule-id-barrel-exports.test.ts,system-fields-consumers.test.tsand ten more read their ownsrc/); the root gates that readpackages/lintall scan many packages at once. So no rootpackage.jsonrow and nolint.ymlstep, which also keeps this diff off two files other seats are editing.Three clauses, all exact in both directions:
object-graph.tsdeclares exactly one coercion and it isrecordsOf.(v: unknown) => AnyRec[]is inCOPY_LEDGER— a new copy fails as unlisted, a re-pointed one fails as a stale row. Matching by SHAPE, not by the nameasArray, so a renamed copy is still caught. 18 rows on the branch head: the two deferred readers plus the 16 of finding: sixteen lint collection readers still hold a private copy of therecordsOfcoercion #15728.UNGUARDED_ALLOWANCE(6 rows on the branch head), and an allowance whose file no longer casts unchecked also fails.The family sweep,
non-record-object-entry.test.ts, parameterised over 28 collections — the top-level keys every re-pointed reader reads, theTYPE_COLLECTIONStablelintLivenessPropertiesdrives its dynamic read from,positions, and five per-object sub-collections — times five non-record shapes, times both assertions. The two residual throwers and the one residual phantom are written down asRESIDUAL_THROWS/RESIDUAL_INVENTEDrows naming their cards, so the sweep reds both when a new one appears and when a filed one is fixed.One existing pin moved:
lint-liveness-properties.test.tsreported a bundle attranslation bundle #2and now reports#1, because that walk renumbers past the two unreadable members. The finding, the rule and the locale are identical; only the positional label moves, which is the trade #15552 settled explicitly ("the index is allowed to differ … a difference in the path, never in whether the object was judged"). Where a suite pins the author-file position instead, it keeps its raw walk — see the two reverted readers above.Verification
Head
4281c5d60, which merges#15741. All numbers below are from that commit; shared-box seconds, several agents on this container. Exit codes captured by redirect-then-capture, never after a pipe.pnpm --filter @objectstack/lint test—Test Files 97 passed (97),Tests 3327 passed (3327), exit 0.pnpm --filter @objectstack/lint typecheck— exit 0,check:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json.pnpm check:pm-dispatch-gates—dispatch-gates self-test: 1478 cases pass, exit 0.pnpm check:nul-bytes— exit 0,check-nul-bytes: OK (scanned 7613 text file(s) … no raw ASCII control bytes).The ordering evidence — both self-expiring lists fired
#15741deletesvalidate-chart-bindings.ts'sasArray. Both of this branch's exact-in-both-directions lists named that file, so merging it made them stale, and both went red saying so by name. That is the mechanism, not an accident, and it is why the two changes could not land in either order without one of them being corrected — which is what this branch did second.Run on the merged tree with the pre-fix gate restored from
HEAD(trap-guarded, mutation and restore both proven on disk, blob hash equal to the HEAD blob):Tests 2 failed | 4 passed (6)—names no copy the ledger has outlivedwith "validate-chart-bindings.ts no longer declares a private coercion. Delete its COPY_LEDGER row", andallows no unchecked branch the allowance has outlivedwith "validate-chart-bindings.ts no longer casts unchecked. Delete its UNGUARDED_ALLOWANCE row".Tests 6 passed (6).RESIDUAL_THROWSrecordedvalidateReferenceIntegritystill throwing onstack.datasets, and after the merge that throw no longer happens, so two cases failed withexpected [] to deeply equal [ 'validateReferenceIntegrity' ]. Those rows are deleted too;objects[].fields/validateStackExpressions(validateStackExpressions throws on a non-record entry of an object'sfields:list — an inline cast the asArray sweeps could not see #15742) stays, still measured as throwing on this tree.Measurements carried forward from the pre-merge head
a0e0b3698Re-stated rather than re-run, because nothing in this ordering fix touches a reader — the two commits since only delete stale rows from two lists.
AUTHORING_RULESover five non-record shapes across 28 collections), the table above: AFTER at head, BEFORE by checking the 20 readers back to1c1421401in the same worktree. The tests import SOURCE via vitest, neverdist/, so no rebuild sits between the legs; restore proven bygit hash-objectequal to the HEAD blob for all 20 and an emptygit diff HEAD.1c1421401): +286 / +1 file, and 286 = 280 (28 collections x 5 shapes x 2 assertions) + 6 (the gate). The head now reads 3327 because the merges brought main's own new tests.validate-semantic-roles.ts— 2 clauses red, both naming the file. Leg 2: add a ledger row forfilter-walk.ts, which declares no coercion —names no copy the ledger has outlivedreds naming it. Each mutation proven on disk by counting the injected marker and the blob hash differing from HEAD; each restore proven bygit hash-objectequal to the HEAD blob, an emptygit diff HEAD, marker count back to 0, and the gate green again. No build sits inside this ablation: the gate imports onlynode:fs/node:path/vitestand readssrc/off disk, so it tests SOURCE and neverdist/.node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack— 53 commands, all run, 51 exit 0, plus the 4 rosters the tool marks as sitting under a directory in this diff,check:changeset-fixed,check:partof-closing-keyword,check:stack-collection-maps,check:self-test-wired— all exit 0. The change set is the same 24 paths after the merge.PREREQUISITE NOT MET, both read a whole-workspace build this worktree does not have):check:dual-build-cjs-loads,check:type-check-debt. Neither is a red. Also NOT MEASURED: the 7 families whose argv takes a value only CI supplies, and the repo-widepnpm lint, left to CI.Findings filed (none fixed here)
recordsOfcoercion #15728 — the 16 remaining copies that do not crash (12 with a local filter, 4 page walks guarded at the call site). Carried inCOPY_LEDGER.recordsOf— dropping a member renumbers the finding paths #15494 pins #15740 — the index-vs-copy collision that blocks the two reverted readers; asks for a maintainer's pick between renumbering, an index-preserving companion inobject-graph.ts, or a permanent exemption.fields:list — an inline cast the asArray sweeps could not see #15742 —validateStackExpressionsthrows onobjects[].fields, an inline cast atvalidate-expressions.ts:137that theasArraygreps behind Twelve more authoring rules crash on a non-record entry in stack.objects — five unguarded readers beyond the indexObjectGraph seam #15552 and Twenty-three more lint collection readers do not filter a non-record entry — every stack collection except stack.objects #15636 could never see.Open question 1 above is #15740, carried to the maintainer by the dispatching seat with recommendation B (an index-preserving companion in
object-graph.ts); it is deliberately not implemented here. Open question 2 is settled: the two-clause gate as shipped is the contract, and #15728 empties the ledger.Not touched:
object-graph.ts,validate-chart-bindings.ts(its re-point is #15741's, merged in),validate-widget-bindings.ts(which carries no such copy), rootpackage.json,lint.yml.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code