Repository navigation
test(lint): pin the dispatch-contract disarm by an undeclared same-named action, and state it in the docblock - #19781
Conversation
… and state it in the docblock `collectDeclaredContracts` pools global and object-embedded actions into one flat name namespace and counts an action that declares no `execution` as a disagreeing declaration, so one undeclared same-named action anywhere in the stack turns `action-dispatch-contract-mismatch` off for that name. The behaviour is deliberate and unchanged; nothing pinned it (deleting the `'none'` arm left the whole package green) and the docblock did not say it. - Five tests: the lit control, an agreeing-sibling control, and the disarm from the view's own object, from an unrelated object and from the global list, each beside the same stack without the sibling. - The docblock now states the mechanism: absence counts as disagreement, declarations are not scoped by the view's object, and why silence is the intended direction. No logic change. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
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): |
Contract reviewServed-tier: Card #17923, one round. Base Tier, measured by the SEAT from the reviewer's transcript (⛔ not its self-report): ① Derived judgments
② Semver levelNo release; ③ Boundary flagsNone tripped. Two files (+107 / −6), no governed path, CI at this head, the reviewer's final reading: 34 names, 28 success + 6 skipped, 0 failure / in progress / queued / cancelled. All seven required contexts Non-blocking, recorded, ⛔ not a round:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #17923
Clause-②: no
What this is
validateActionDispatchContractgoes silent for an action name when any same-named action in the stack declares noexecution. That behaviour is deliberate and stays as it is. This PR adds pins for it and rewrites thecollectDeclaredContractsdocblock to explain the mechanism. No logic change: every changed line invalidate-action-dispatch-contract.tsis a comment line, and the built JS and.d.tsare byte-identical to the base (see Changeset below).The mechanism, checked on the base
collectDeclaredContractsputs global and object-embedded actions into one flat name namespace. When an action declares nothing,contractOf(a.execution) ?? 'none'adds'none'to that name's contract set. Thenif (contracts.size !== 1) continue;drops the name. So a missing declaration counts as a disagreeing one. I checked this at the claim's base041c8cf62band again at this branch's base70a2e752b8. Between those two commitspackages/linthas no diff.Changes
packages/lint/src/validate-action-dispatch-contract.test.tsgets a newdescribeblock with five tests. They share one stack builder: one view ontaskwiresrecalcas a bare string, andinvoiceis an object the view never resolves through.recalcdeclaredaggregateis refused (1 finding, atviews[0].list.bulkActions[0]).taskand again oninvoice, leaves that finding in place. So the silence comes from the sibling's missing declaration, not from a second declaration being there.recalcembedded intaskgives 0;recalcon the unrelatedinvoicegives 0;recalcnext to an object-embedded declaration gives 0.packages/lint/src/validate-action-dispatch-contract.ts: thecollectDeclaredContractsdocblock now says it directly:It keeps the existing reasoning (zero false positives, silence as the safe direction, "undeclared is not defaulted"). It adds one point: treating an undeclared sibling as agreement would bring back that silent default through the sibling.
The third option from the review is not in this PR: scoping declarations by the view's object would change behaviour.
Ablation: the pins can fail
const declared = contractOf(a.execution) ?? 'none';becomesconst declared = contractOf(a.execution); if (declared === undefined) return;, which deletes the'none'arm.scripts/ablation-replace.mjsin wrap mode. On disk: anchor 1 to 0, marker 0 to 1, blob3be3e4678b42to2bcd771ce874.git checkout HEAD -- ABS_PATH. It was checked by blob hash againstHEAD:plus an emptygit diff HEAD, and a shell trap covers EXIT/INT/TERM.src/, not throughexports/dist.041c8cf62b(claim base)@objectstack/lintsuite, mutated4433ece870(head)validate-action-dispatch-contract.test.ts, mutated4433ece8703be3e4678b42equalsHEAD:, andgit diff HEADis emptyChangeset: none (the
skip-changesetroute)@objectstack/lintshipsfiles[]=dist,README.md,CHANGELOG.md. It does not shipsrc/**.ABSENCE COUNTS AS DISAGREEMENT,arriving through a siblingandundeclared is not defaultedget 0 hits;action-dispatch-contract-mismatchin 6 files andcollectDeclaredContractsin 4.index.js,index.cjs,runtime.js,runtime.cjsand all four.d.ts/.d.ctsfiles have identical sha256. Only the sourcemap line mappings move, and the maps carry no source text (check:sourcemap-no-sources-contentgreen).So nothing in this PR reaches a consumer. It takes the
skip-changesetroute, and the seat applies that label.Verification (head
4433ece870)pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 108 files, 4116 passed. That is the base's 4111 plus the 5 new tests. The base's 5 skips ran here because the lintdist/was built for this run.pnpm --filter @objectstack/lint run typecheckexits 0.check:test-typecheckis OK, its ledger is unchanged, and--listFilesshows this test file is in thetsconfig.test.jsonprogram with 0 errors against it.eslint --no-inline-config --format jsonon the two files: 2 files, 0 errors, 0 warnings;--print-configresolves 6 and 5 rules for them, so neither file is ignored;parserOptions.projectandprojectServiceare both null). So this diff cannot change the verdict for any file it does not touch. The repo-widepnpm lintis left to CI.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 52 commands.--ranreconciles them as 50 run at exit 0, 2 NOT MEASURED, and 0 unrun.check:lean-entry-closurefirst exited 3 (prerequisite). It passed after building the@objectstack/objectqlclosure.check:dual-build-cjs-loadsandcheck:type-check-debtboth exit 3. They need the whole workspace built (turbo run build --filter='./packages/*' --filter='./packages/*/*'). The only package this diff touches has byte-identical JS and.d.tsoutput, and its build-config source change is comment-only. CI'sLint & Repo Gatesruns both on a fresh full build.packages/path also pass:check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity.Acceptance notes
execution". The claim limited this round to thecollectDeclaredContractsdocblock, so the header is unchanged. The docblock is where the mechanism is now stated.041c8cf62bonto70a2e752b8before its first non-empty push, because the gate derivation flagged the tree as stale. The push was a fast-forward from the empty-branch push; nothing was force-pushed.Generated by Claude Code