fix(spec): defineStack strict:false refuses a non-array objects with an ADR-0112 envelope - #19798
Conversation
…the ADR-0112 envelope mergeActionsIntoObjects now refuses every non-array `objects` except an absent one with StackSchemaInvalidError (STACK_SCHEMA_INVALID, 422, zod issue at ['objects']) instead of raising a bare TypeError from `.map`, and hands a non-object entry on untouched instead of dereferencing it. Claude-Session: https://claude.ai/code/session_01VWsFyWDp8Rjb2Ma6a3Cyo8 Co-authored-by: Claude <noreply@anthropic.com>
…rge-actions-shape-refusal
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 136 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 c0088ebf985296094a91c552e1484b708f76473d && git checkout c0088ebf985296094a91c552e1484b708f76473d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0b83e01627dbf2dce05cb38148fecd3cff430696 ce893bbf31e7068187fb1b0ad4677f2d20771dfe && git checkout -B drift-repro 0b83e01627dbf2dce05cb38148fecd3cff430696 && git merge --no-ff ce893bbf31e7068187fb1b0ad4677f2d20771dfe
node scripts/docs-audit/affected-docs.mjs --json 0b83e01627dbf2dce05cb38148fecd3cff430696 |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Minimal fixes: (1) refuse a non-object entry with the same envelope ( Rendered by an isolated at-tier review subagent, adopted by Generated by Claude Code |
…rge-actions-shape-refusal
…y too A non-object entry in `objects` is now refused with the same envelope (STACK_SCHEMA_INVALID, 422, one zod issue per entry at ['objects', index], expected 'object') instead of being handed on untouched, which widened the accept set and let plugin-object registration drop every later object. Claude-Session: https://claude.ai/code/session_01VWsFyWDp8Rjb2Ma6a3Cyo8 Co-authored-by: Claude <noreply@anthropic.com>
…rge-actions-shape-refusal
…beNonArrayCollection Claude-Session: https://claude.ai/code/session_01VWsFyWDp8Rjb2Ma6a3Cyo8 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Round-1 re-review by an isolated at-tier subagent, adopted by Generated by Claude Code |
Fixes #19785
Clause-②: no (narrowing)
defineStack(config, { strict: false })now refuses a non-arrayobjects, and anobjectsarray holding a non-object entry, with the ADR-0112 envelope:StackSchemaInvalidError,code: 'STACK_SCHEMA_INVALID',status: 422, and the zod issue atpath: ['objects'](non-array) or one per entry atpath: ['objects', index]withexpected: 'object'(non-object entry). Before, a number or a string raised a bareTypeError: config.objects.map is not a functionfrommergeActionsIntoObjects, and anullentry raised one readingactionsoff it. No new error code; the error class stays module-local.Refuse or skip: the card's first question
Refuse. Here is what
DefineStackOptions.strictdeclares forfalse: "validation is skipped for maximum flexibility (e.g., when views reference objects provided by other plugins). Use this ONLY when you need to bypass validation for advanced use cases." It promises that validation is skipped: cross-references and schema detail. It does not promise to accept, or quietly skip, a shape the action merge cannot read. No ADR is cited there. So this is not a contract-direction question, and the #18239 ruling (5690859601) applies one door earlier: 「an ADR-0112 envelope error (closederror.code, status), ⛔ not a bareTypeErrorand ⛔ not a skip」.What changed
packages/spec/src/stack.zod.ts, insidemergeActionsIntoObjectsonly:objectsthat is present and not an array is refused.undefinedis the only non-array that is not malformed, because the key is absent. This is the line fix(spec): composeStacks refuses a non-arrayobjectswith an ADR-0112 envelope #19783 drew formergeObjects. It includes the falsy valuesnull,'',0andfalse, which used to be handed on untouched and then refused one call later bycomposeStackswith this same code. Refusing5while passing0would repeat the silent-skip row fix(spec): composeStacks refuses a non-arrayobjectswith an ADR-0112 envelope #19783 removed. The map form ({ name: { ... } }) is normalized to an array before the merge and is still accepted.null,7, a string, an array) is refused with the same envelope: one zod issue per such entry at['objects', index],expected: 'object'(read fromz.array(z.looseObject({})), re-rooted atobjects, which is the strict parse's own answer for the shape). The message names each position. Round 0 handed such an entry on untouched. The contract review showed that this widens the accept set under a(narrowing)declaration: the card's own[null, obj]repro threw before and would have succeeded. It would also let the next consumer, plugin-object registration inpackages/objectql/src/engine.ts(one loop in one warn-level catch), silently drop every object after the bad entry. Now every row narrows.Measured on the base
3f9e2eaa1c, against the builtdist:objectsunderstrict: false5,'abc'TypeError: config.objects.map is not a function,code/statusundefinedSTACK_SCHEMA_INVALID, 422null,'',0,falseSTACK_SCHEMA_INVALID, 422[null, obj]TypeError: Cannot read properties of null (reading 'actions')STACK_SCHEMA_INVALID, 422, issue at['objects', 0][obj, 7]STACK_SCHEMA_INVALID, 422, issue at['objects', 1]Tests
packages/spec/src/define-stack-non-strict-objects-shape-refusal.test.ts. It has 6 non-array refusal rows assertingcode+status+ the issue (path,invalid_type,expected: 'array'). Two entry refusal rows cover[null, obj, 7], with issues exactly at['objects', 0]and['objects', 2], allinvalid_type/expected: 'object', plus[null, obj]. Controls: array, map form and absentobjectsare all accepted with the bound action merged, and the strict door raises the same code for the same shape.packages/spec/src/compose-stacks-objects-shape-refusal.test.ts(fix(spec): composeStacks refuses a non-arrayobjectswith an ADR-0112 envelope #19783's file): its four falsy rows reached composition throughstrict: false. That door now refuses those shapes itself, so the rows now reach composition as hand-built stacks. They keep the same values and the same assertions. This is a fixture re-route, and it is outside the claim's declared file surface. I am declaring it here; the contract review accepted it.d3959b7b79):stack.zod.tsrestored from the base, with the new file run against it:Tests 7 failed | 4 passed (11). The refusal rows failed withexpected undefined to be 'STACK_SCHEMA_INVALID'and the entry row with a bareTypeError. Restored,git diff HEADempty.c5a865c769):scripts/ablation-replace.mjsreplaced the entry guard's condition. The anchor went x1 to x0 and the blob88f36c9846becameac5fbf58be. Result:Tests 2 failed | 10 passed (12). Both entry rows failed withexpected undefined to be 'STACK_SCHEMA_INVALID'(a bareTypeErrorreached the assertion), and every other row stayed green. Restore: blob back to the HEAD blob88f36c9846,git diff HEADempty.c5a865c769(merge oforigin/mainfae870352e):pnpm --filter @objectstack/spec build+typecheck: exit 0. The test layer compiles, andtest-typecheck-debt.jsonheld.Test Files 552 passed (552),Tests 15690 passed | 1 todo.--no-inline-configover the 3 touched TS files: 3 files, 0 errors, 0 warnings. Type-aware linting is not enabled ineslint.config.mjs, so this diff cannot move the verdict on any untouched file.dispatch-gates --commandsderived 82 commands (the same list as round 0). 80 exit 0. 2 are NOT MEASURED with exit 3 (they need the whole-workspace build):check:dual-build-cjs-loadsandcheck:type-check-debt.--ranreconcile: 82 accounted, 0 UNRUN.ce893bbf31(merge oforigin/main0b83e01627, which includes fix(spec): composeStacks refuses a non-array concatenated collection instead of dropping its content #19794, then the helper routing):compose-stacks-concat-shape-refusal.test.ts: 162/162.Test Files 553 passed (553),Tests 15815 passed | 1 todo.Tests 6 failed | 6 passed (12)(all 6 non-array rows). Ablation of the entry guard:Tests 2 failed | 10 passed (12). Both restores ended at blob == HEADac2a452f57,git diff HEADempty.origin/mainhas since moved one commit,de4ed33fd5(docs(pm), docs(pm): a suspended seat account is a dead session — takeover, re-rendered records, retriage, durable state #19795). It is not merged.Acceptance notes
class: a, not fixed here, filed by the seat as [finding]defineStack(config, { strict: false })still raises a bare TypeError for a non-arrayactions(top-level or on an object) —sortActionsByOrdercalls.someunguarded; the sibling of #19785 one key over #19799. The same function still dereferences the other shapes it reads understrict: false: a top-levelactionsof5or'abc', oractions: [null], raises a TypeError fromsortActionsByOrder.ce893bbf31. PR fix(spec): composeStacks refuses a non-array concatenated collection instead of dropping its content #19794 landed (0b83e01627), so the non-array block now calls itsdescribeNonArrayCollection('objects', …)helper, with no hand-copied kind/issues code left. Messages are byte-stable for every input that can reach this door. The helper's one extra branch,'an object'for a plain object, is unreachable here becausenormalizeStackInputturns the map form into an array first.strict: falsewith aSetasobjectsstill returns without throwing; the normalizer reads it as a keyless map. This was recorded on fix(spec): composeStacks refuses a non-arrayobjectswith an ADR-0112 envelope #19783 and is not part of this card.Generated by Claude Code