validateActionDispatchContract is silently disarmed for a name when a same-named action exists that declares no execution — including when that sibling sits on an object the view cannot resolve through. The behaviour is deliberate and zero-false-positive, but it is half pinned and half documented.
Measured
Surfaced by the at-tier contract review of PR #17912 (card #17319), 2026-09-13T04:37:53Z, with its own probe (probe-disarm.mts via tsx) and a lit control:
| probe |
result |
CONTROL — global recalc declared aggregate, wired as a bare string (a real mismatch) |
1 finding |
same, plus an object-embedded recalc with no execution |
0 findings |
| same, but the undeclared sibling sits on an unrelated object the view cannot resolve through |
0 findings |
⇒ The control fires, so the zeros are readings. collectDeclaredContracts (validate-action-dispatch-contract.ts:160-164) drops a name whose declarations disagree and counts "declares no execution" as disagreeing — so one undeclared sibling anywhere in the flat action namespace turns the rule off for that name.
⚠️ The shape is legal and reachable: validate-action-name-refs.ts:77 treats global and object-embedded action names as one flat namespace.
What is missing — ⛔ not a request to change the behaviour
The posture is defensible: silence is the safe direction for a zero-false-positive rule, and it does not touch any schema. ⛔ This card does not ask for the disarm to be removed. It asks for the two things that would make it survive its author:
- No pin.
validate-action-dispatch-contract.test.ts:156 pins the aggregate-vs-perRecord disagreement. Nothing pins "declared + undeclared sibling ⇒ silent." Delete the absence-counts-as-disagreement branch and no test reds.
- The docblock understates it.
:133-138 says the rule "contributes only when every declaration agrees" — it does not say that absence counts as a disagreeing declaration, which is the whole mechanism.
⚠️ A third option the review raised and did not settle: scope declarations by the view's object, so a sibling on an unrelated object cannot disarm anything. That is a behaviour change with its own population question ⇒ ⛔ not folded in here.
⛔ Why it was not fixed in PR #17912
The review graded it not a blocker: it is orthogonal to that PR's clause-② widening (one optional key on ActionSchema), and folding an unrelated repair into a clause-② PR is what the review process exists to prevent. That PR is otherwise cleared.
Related, ⛔ not a duplicate
#17916 — the object-grid page-component tier (bulkActions / bulkActionDefs / batchActions) is walked by no reference-integrity rule. That is a tier that is out of scope; this card is about a name being disarmed inside a tier the rule does walk. Same rule, different defect; the review listed them as two follow-ups.
Duplicate check
Title census over the 600 newest issues and PRs, 2026-09-13T04:4xZ: no title carries disarm, and bulkactions returns 1 (#17319, the parent card) with batchactions 0 while contract-review returns 6 — the neighbouring terms fire, so the zero is a reading.
Provenance
Filed by the domain:spec execution seat from the at-tier review's finding, 2026-09-13T04:4xZ. ⛔ Bare and ungraded — no domain:*, no priority:*; both are the triage seat's sole production. Type prefilled only.
Generated by Claude Code
validateActionDispatchContractis silently disarmed for a name when a same-named action exists that declares noexecution— including when that sibling sits on an object the view cannot resolve through. The behaviour is deliberate and zero-false-positive, but it is half pinned and half documented.Measured
Surfaced by the at-tier contract review of PR #17912 (card #17319), 2026-09-13T04:37:53Z, with its own probe (
probe-disarm.mtsviatsx) and a lit control:recalcdeclaredaggregate, wired as a bare string (a real mismatch)recalcwith noexecution⇒ The control fires, so the zeros are readings.
collectDeclaredContracts(validate-action-dispatch-contract.ts:160-164) drops a name whose declarations disagree and counts "declares noexecution" as disagreeing — so one undeclared sibling anywhere in the flat action namespace turns the rule off for that name.validate-action-name-refs.ts:77treats global and object-embedded action names as one flat namespace.What is missing — ⛔ not a request to change the behaviour
The posture is defensible: silence is the safe direction for a zero-false-positive rule, and it does not touch any schema. ⛔ This card does not ask for the disarm to be removed. It asks for the two things that would make it survive its author:
validate-action-dispatch-contract.test.ts:156pins theaggregate-vs-perRecorddisagreement. Nothing pins "declared + undeclared sibling ⇒ silent." Delete the absence-counts-as-disagreement branch and no test reds.:133-138says the rule "contributes only when every declaration agrees" — it does not say that absence counts as a disagreeing declaration, which is the whole mechanism.⛔ Why it was not fixed in PR #17912
The review graded it not a blocker: it is orthogonal to that PR's clause-② widening (one optional key on
ActionSchema), and folding an unrelated repair into a clause-② PR is what the review process exists to prevent. That PR is otherwise cleared.Related, ⛔ not a duplicate
#17916 — the object-grid page-component tier (
bulkActions/bulkActionDefs/batchActions) is walked by no reference-integrity rule. That is a tier that is out of scope; this card is about a name being disarmed inside a tier the rule does walk. Same rule, different defect; the review listed them as two follow-ups.Duplicate check
Title census over the 600 newest issues and PRs, 2026-09-13T04:4xZ: no title carries
disarm, andbulkactionsreturns 1 (#17319, the parent card) withbatchactions0 whilecontract-reviewreturns 6 — the neighbouring terms fire, so the zero is a reading.Provenance
Filed by the
domain:specexecution seat from the at-tier review's finding, 2026-09-13T04:4xZ. ⛔ Bare and ungraded — nodomain:*, nopriority:*; both are the triage seat's sole production. Type prefilled only.Generated by Claude Code