fix(spec): every defineStack refusal carries an ADR-0112 envelope — six STACK_* codes beside STACK_CROSS_REFERENCE_INVALID - #16342
Conversation
…ix STACK_* codes beside STACK_CROSS_REFERENCE_INVALID The six remaining bare-Error refusal sites in defineStack (schema parse, capability, namespace-prefix, single-app, hierarchy-scope capability, trigger capability) now throw module-local envelope classes sharing a StackRefusalError base: status 422, one code per site, findings on issues. Message text is byte-for-byte unchanged at every site. The schema arm is its own code (STACK_SCHEMA_INVALID) on a reading taken before writing it: spec has no zod-failure envelope to reuse, the ledger's two zod-shaped refusals are both *_SCHEMA_INVALID at 422, and the request- syntax (VALIDATION_ERROR) and record-validation (VALIDATION_FAILED, duck- typed on name === 'ValidationError') channels would each mis-file an authored stack. One classification row per new code in the runtime dispatcher error-code vocabulary (door none, verdict boot-refusal), with the reachability measurement re-taken on this tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…ing prose check:doc-authoring refuses an issue id inside sibling-package string prose (a runtime string reaches authors who cannot resolve #NNNN); the ADR anchor stays, the tracker ids move out of the strings. The comment header above the rows keeps its id — comments are the reader who can. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…finestack-refusal-envelopes
📓 Docs Drift Check15 anchor(s) derived from 2 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 — 135 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 680887ab3f1490a4ae616b5b4578bc051eabe35d && git checkout 680887ab3f1490a4ae616b5b4578bc051eabe35d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 60ff091a99287003b22a4138db9cdee3b9f927aa da4d62b154f8a492b17695e97315de19816a9104 && git checkout -B drift-repro 60ff091a99287003b22a4138db9cdee3b9f927aa && git merge --no-ff da4d62b154f8a492b17695e97315de19816a9104
node scripts/docs-audit/affected-docs.mjs --json 60ff091a99287003b22a4138db9cdee3b9f927aa |
…422 claim on METADATA_SCHEMA_INVALID Clause-② is yes (six new error codes ship in spec's dist and cannot be renamed once consumers branch on them), and a purely additive widening of a published package's public surface takes at least minor — the commit type may raise a bump but never lower it below what the act requires (maintainer ruling 2026-09-04, decision batch #35). Both packages move from patch to minor; the changeset records why. Review advisory A1: nothing in the tree assigns METADATA_SCHEMA_INVALID a status — it stays the issues-carrying precedent, FLOW_INPUT_SCHEMA_INVALID carries the 422 (flow-dispatch-status.ts), and the zod-shaped refusal metadata-protocol stamps at 422 is INVALID_METADATA. A2: the base class docblock states that issues is heterogeneous by design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…finestack-refusal-envelopes
…finestack-refusal-envelopes
|
PM — flipped to ready and auto-merge armed at 19:24:12Z. The body's Carrier discipline paragraph ("not flipped to ready and not enqueued") described the state when it was written and is now superseded by this comment; it is not rewritten, so the record of what the dev handed over stays intact. Why now. The three landing preconditions:
None of the four files touch the governed surface, so this is not a governed-surface merge. On the refresh itself: it was a merge, never a rebase, amend or force-push — the push was the fast-forward If Generated by Claude Code |
…finestack-refusal-envelopes
Fixes #15963
Clause-②: yes
What changed
defineStackhas seven refusal sites. After #14552 one of them — the cross-reference refusal — carried an ADR-0112 envelope (code/status); the other six still threw a bareErrorwith bothundefined. A consumer that had learned to branch onerror.codereadundefinedfrom six of the seven, which reads as "not a validation refusal" rather than "a refusal with no code yet". Every site now throws a module-local envelope class sharing oneStackRefusalErrorbase:status: 422, onecodeper site, the findings the site collected onissues. Message text is byte-for-byte unchanged at every site — the pins instack.test.ts,stack-requires.test.tsand the objectql matrix test read the same prose they always did.e6f7ccb54a)code:2725ObjectStackDefinitionSchema.safeParse(formatZodError)STACK_SCHEMA_INVALIDError:2741validateKnownCapabilitiesSTACK_CAPABILITY_UNKNOWNError:2751validateCrossReferencesSTACK_CROSS_REFERENCE_INVALID:2760validateNamespacePrefixSTACK_NAMESPACE_PREFIX_INVALIDError:2767validateSingleAppSTACK_SINGLE_APP_VIOLATIONError:2774validateHierarchyScopeCapabilitySTACK_HIERARCHY_SCOPE_CAPABILITY_REQUIREDError:2781validateTriggerCapabilitySTACK_TRIGGER_CAPABILITY_REQUIREDErrorOne code per site, never a shared
STACK_VALIDATION_FAILED: the dispatcher vocabulary'sboot-refusalclass was already at one-row-per-refusal granularity (14 rows), andSTACK_CROSS_REFERENCE_INVALIDis an instance of that granularity, not an exception — executing the answered decision, not re-opening it. Spellings follow the ledger's own suffix vocabulary, measured overERROR_CODE_LEDGER:_INVALID(7 members),_REQUIRED(11, e.g.TENANT_SCOPE_REQUIRED),_UNKNOWN(FILTER_TOKEN_UNKNOWN),_VIOLATION(UNIQUE_VIOLATION,EXTERNAL_SCHEMA_MODE_VIOLATION).StackCrossReferenceErrorkeeps its code,nameand message and now extends the same base; nothing is exported (the module is re-exported withexport *, socheck:api-surfaceis unchanged — verified against a freshdist). No class is namedValidationError:validationFailureDetailsin@objectstack/typesduck-types a record-validation failure on thatnameand would answer400 VALIDATION_FAILED+fields[]; the base class documents the trap and the new test pins it.issuesis heterogeneous by design — one string per finding on the six semantic refusals, the zod issue objects on the schema arm — and the base-class docblock says so (review advisory A2).packages/runtime/src/dispatcher-error-vocabulary.tscarries one classification row per new code (shape: 'classfield',door: 'none',verdict: 'boot-refusal'), with the reachability measurement re-taken on this tree rather than inherited.Readings — re-measured on this branch, not inherited
origin/main6c546ab9d0before any edit, so feat(spec): registerNAMESPACE_CONFLICTinERROR_CODE_LEDGER#16252 (which removed theNAMESPACE_CONFLICTpending row, 42 lines) is under every measurement. At that base the seven sites sat at:2553 :2566 :2576 :2584 :2591 :2598 :2605and the class at:1792— the dispatch's numbers held.readonly code =instack.zod.ts: 1 → 7; files carrying it acrosspackages/spec/src: 3 → 3 (connector-provider-errors.ts,external-errors.ts,stack.zod.ts). Barethrow new Error(insidedefineStack: 6 → 0.verdict: 'boot-refusal'rows 14 → 20;STACK_*rows 1 → 7; the gate's census 65 → 71 classified sites, still2 awaiting a ledger entry.defineStackoccurrences underpackages/runtime/src+packages/rest/src= 33, every one a docstring, a comment or the vocabulary table's own prose — zero call sites (the precedent row recorded 25 on its tree)..codeon none of these throws today (packages/cli/srcmeasured): the card's consumer is the branch this change makes possible, not one that exists.The
:2553judgement — its own arm,STACK_SCHEMA_INVALID(reading taken before writing)The schema site is an aggregate of zod issues against the schema the stack declares, not a rule evaluated on a parsed stack, so it was judged on its own rather than copied. Four options, each along the four axes:
STACK_SCHEMA_INVALID, 422,issues= the zod issues structurally. 长远: one closed shape for all seven refusals, and the shape the tree already uses for "an entity contradicts the schema it declares" — the ledger's two zod-shaped refusals are both spelled*_SCHEMA_INVALID:METADATA_SCHEMA_INVALID(SchemaValidationErrorin@objectstack/metadata-core, theissues-carrying precedent — its docstring says 422 but nothing in the tree assigns it a status, per review advisory A1) andFLOW_INPUT_SCHEMA_INVALID(answered 422 bypackages/runtime/src/flow-dispatch-status.ts); the zod-shaped refusalmetadata-protocolactually stamps at 422 isINVALID_METADATA(protocol.ts,runtime-authoring-gate.ts). So theissuesshape comes from the first and the 422 from the other two. 防 AI 写错: structurally distinct from every other channel;undefinedis no longer a legal answer at any site. 业务: the population isos validate/os build/ host boot and the AI author —skills/objectstack-platform/SKILL.mdline 89 shows this exact header to authors. 创业阶段: one class, no new mechanism, no export, no transition.formatZodError/safeParsePrettyreturn prose; everyextends Errorinpackages/spec/srcis a domain refusal and none wraps aZodError. The only existing channel is the message — the defect itself.400 VALIDATION_ERROR(status-derived, no own code). That is the request-syntax bucket (packages/restanswers a malformed body with it, passing raw zod issue codes as field errors — ADR-0112 D6).defineStackserves no request, and a barestatuswith nocodeleaves the throw'scodeundefined— the exact shape the card measured.VALIDATION_FAILED+fields[]viazodIssuesToFields. objectql's record-validation vocabulary, recognised by duck-typing oncodeorname === 'ValidationError'. An authored stack would be indistinguishable from a refused record write to every reader of the dispatcher's envelope — the conflation anerror.codeexists to prevent, and the 防 AI 写错 axis rules it out on its own.issuescarries the zod issues (path, code, message per entry) rather than the formatted lines: the message already renders those,SchemaValidationError.issuescarries the same, andresolveThrownHttpErroralready reads a thrown.issuesarray as spec-validation context. The header's count isissues.lengtheither way, which the test pins.Changeset —
@objectstack/specminor,@objectstack/runtimeminorRaised from the
patchthe #14552 precedent took, because the level is required, not chosen: six newSTACK_*spellings ship inpackages/spec/dist/index.jsanddist/index.mjs, nothing reaches.d.ts, and once shipped a consumer'scatch (e) { switch (e.code) … }depends on them and they cannot be renamed. That is a purely additive widening of a published package's public surface, and the maintainer ruling of 2026-09-04 (decision batch #35, on #15294) requires at leastminorfor it — the commit type may raise a bump but never lower it below what the act requires. Thepatchprecedent is pre-rule.Check Changesetenforces this on any PR that declares clause-②yes, which is what theClause-②: yesline at the top of this body declares durably, independent of the carrier label.Verification (gate verdict lines, exit codes read after redirect, never through a pipe)
Tests, at
48e462172f(spec sources byte-identical at273247e56f; the follow-upe6f7ccb54achanges spec only inside docblocks — 0 non-comment lines in itsstack.zod.tsdiff — so no spec rebuild was owed for it):pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2overstack-refusal-envelopes.test.ts,stack-cross-reference-envelope.test.ts,stack-requires.test.ts,stack.test.ts—Test Files 4 passed (4),Tests 149 passed (149); the new file alone27 passed (27).pnpm --filter @objectstack/objectql exec vitest run src/registry-cross-package-item-classes.test.ts(the consumer pinning the precedent envelope, read through a freshly built@objectstack/specdist) —14 passed (14).pnpm --filter @objectstack/spec typecheck—VERDICT command-exit 0(tsc,check:scripts-typecheck,check:test-typecheck: OK — @objectstack/spec's test layer compiles, no debt-ledger change).tsc -pover exactly that file (program = 1 runtime source,--listFiles) exits 0. CI runs the full program.Gates, on
273247e56fand re-run one6f7ccb54a:pnpm check:dispatcher-error-vocabulary—OK — 71 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry.pnpm check:doc-authoring—sibling-package prose ids hold the baseline — no growth(a first run at48e462172fwas a real red: tracker ids inside the sixwhystrings; fixed in20556ee3af).pnpm check:error-code-casing—no unlisted lowercase error codes in 5706 scanned file(s);pnpm check:nul-bytes—OK ... no raw ASCII control bytes.pnpm --filter @objectstack/spec buildthencheck:generated—All 15 generated artifacts are up to date(check:api-surface,check:authorable-surface,check:docsincluded).scripts/pm/dispatch-gates.mjs --ran—73 derived, 73 run, 0 UNRUN. Two of the 73 returned the prerequisite exit 3 in this partially built worktree and are NOT MEASURED here, owned by CI:check:dual-build-cjs-loads(51 packages withoutdist/) andcheck:type-check-debt(needs every closure built).pnpm lintnarrowed and proven: eslint over the three changed TS files (--format json: 3 files, 0 errors, 0 warnings); population read fromeslint.config.mjs(packages/**/*.{ts,tsx,mts,cts}covers all three); invariance: the config declares noparserOptions.projectand no typed rules, so this diff cannot move any untouched file's verdict. The repo-wide scan is CI's.check-changeset-no-major.mjs --eventone6f7ccb54a(the gate reads the changeset at the committed head): a payload with no label and a body carryingClause-②: yes→✓ LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/*/src/** it moves is graded patch, readingdeclaration line: Clause-②: yes— the gate fires on the line alone; the same payload on273247e56f(stillpatch) →⛔ ... grades a package it grew patch, exit 1; a payload with neither label nor line →LEVEL AXIS: NOT MEASURED, exit 0 — the blind green, not a pass.Ablation — direction predicted before each run, restore proven
The gate reads source, so no
dist/leg applies; each mutation is proven on disk by occurrence counts, restored withgit checkout HEAD -- path, and the restore proven by blob hash equal toHEAD's plus an emptygit diff HEAD.STACK_TRIGGER_CAPABILITY_REQUIREDledger row (rows 1 → 0, 17 lines). Predicted: red, naming an unclassified stamping site. Observed: exit 1,[unclassified-site] packages/spec/src/stack.zod.ts stamps unregistered code 'STACK_TRIGGER_CAPABILITY_REQUIRED' (classfield) and packages/runtime/src/dispatcher-error-vocabulary.ts does not classify it.Restored: blobed927db5…= HEAD.[stale-row] ... declares 'STACK_SINGLE_APP_VIOLATION' at packages/spec/src/stack.zod.ts (classfield) but the scan no longer finds it— plus a second diagnostic I had not predicted,[unresolved-constant]on the marker. Restored: blob8d26555e…= HEAD.Scope notes
composeStacksin the same file still carries bare refusals (the action-key collision at its tail and the object-source internal error). My judgement: the action-key collision is the same class as these seven (an authored entity refused at authoring, reachable through the same callers) and belongs in this family under its ownSTACK_*code; the object-source line is an internal invariant (composeStacks internal error), not an authored-entity refusal, and would be mis-filed at 422. Not folded in — reported for a card.ManifestSchemaan open object after #14192 closed it —stack.zod.ts,compile.ts, coreartifact-packages.ts, the strictness ledger's own prose, and themaindescribe's loose ADR-0025 citation #14721 is not addressed here. Its docblock sentence onAssembledPackageBodySchemais untouched, and nothing in this diff changes whether that sentence is true.origin/mainis merged in (textual merges, no conflicts):3e270d4e29before the PR opened, and again after this body edit so theCheck Changesetgate — which runs onsynchronize, not onedited— re-reads the body carrying theClause-②: yesline.Carrier discipline
Draft, dispatched at
CONTRACT_REVIEW_TIER; the review passed at tier with no blockers and the PM clearedneeds:contract-reviewfrom this PR afterwards, which is correct carrier discipline — the durable clause-② declaration is the line at the top of this body, not the label. This PR is not flipped to ready and not enqueued.