fix(spec): defineStack's cross-reference refusal carries an ADR-0112 envelope - #15962
Conversation
…e refusal (#14552) RED against origin/main d4c2cb1: 13 failed / 7 passed. The 7 passing are the message-text pins, which prove every fixture reaches the cross-reference gate for the right reason; the 13 failures are the envelope assertions themselves (code and status both undefined). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…envelope (#14552) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…finestack-crossref-error-codes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
📓 Docs Drift Check3 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 e81fcba713174fb5e4504a77c2b976caee6216b6 && git checkout e81fcba713174fb5e4504a77c2b976caee6216b6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3f89967e35acd69cbd9be7310fdb6ee6d83ca93e 24af8e52a937c5e3ee23c5b8f1d2adb4082a220a && git checkout -B drift-repro 3f89967e35acd69cbd9be7310fdb6ee6d83ca93e && git merge --no-ff 24af8e52a937c5e3ee23c5b8f1d2adb4082a220a
node scripts/docs-audit/affected-docs.mjs --json 3f89967e35acd69cbd9be7310fdb6ee6d83ca93e |
Docs Drift Check close-out — a NULL result, stated as a measurementAddendum to the report above, at PR head ⛔ I did not rely on the drift bot's output for any part of this. It reported that its own checkout carried uncommitted changes, so the commit it names does not fully identify what it read; this reading is my own, per-file, on this branch at a known sha. MethodProbes built the candidate set; the verdict on each candidate comes from READING that page, not from the grep that surfaced it. Probes run: the six verbatim message fragments this raiser emits; the shape vocabulary ( Verdict: nothing published is falsifiedThe decisive negative. Pages read per-file, and why each stands:
Preserving the message byte-for-byte is what makes the two message-publishing pages survive; had the prose been reworded, both would have been in scope under E3.
E3 disposition「已发布必修」has nothing to fix: no published page is falsified by this change. So no docs edit belongs in this PR, and no docs card is owed either. Adding new documentation for One incidental corroboration from the read: Generated by Claude Code |
Closes #14552
validateCrossReferences— reached throughdefineStack— refuses a stack whose items name an object the stack does not define. That refusal wasnew Error(message)withcodeandstatusbothundefined, so all five REFUSED item classes of the ADR-0130 matrix plus thehooks[].objectrule (#14122 §4 rule R4) were distinguishable only by MESSAGE TEXT. It now throwsStackCrossReferenceError:code: 'STACK_CROSS_REFERENCE_INVALID',status: 422, one entry per finding inissues, message byte-for-byte unchanged.Repaired following the two landed precedents for this defect class, one door over: #14367 (
ObjectOwnershipConflictError) and #14474 (NamespaceConflictError). Their conventions AGREE on every point that governs this change.Tier: no new catalog member — the published contract face is NOT widened
The dispatch framed this as a binary: reuse an existing ADR-0112 catalog member (lands), or add a new one (parks under
needs:contract-review). Measured on the tree, the precedents take a third route, and it is the settled one:StandardErrorCodeERROR_CODE_LEDGEROBJECT_OWNERSHIP_CONFLICT(#14367)NAMESPACE_CONFLICT(#14474)STACK_CROSS_REFERENCE_INVALID(this PR)Neither precedent registered its code.
ErrorCode=StandardErrorCode∪ERROR_CODE_LEDGERis the wire vocabulary; a refusal no wire door raises does not enter it. #14474's changeset states the rule outright: "no ledger registration ... Not narrowed, not widened." Instead each carries a classification row inpackages/runtime/src/dispatcher-error-vocabulary.ts, whichpnpm check:dispatcher-error-vocabularyenforces both ways.So this PR adds no catalog member, and
check:api-surfaceis green with nothing regenerated. ⇒Clause-②: no, re-declared from the delivered diff.Reusing an existing member was considered and rejected on accuracy:
INVALID_REFERENCEis documented as a foreign-key reference at the data layer andOBJECT_NOT_FOUNDas a runtime 404 lookup; neither names an authoring-time dangling declaration.Reachability: measured, not inferred
door: 'none',verdict: 'boot-refusal'.defineStackis an authoring/boot-time entry point. Every non-test occurrence ofdefineStackunderpackages/runtime/srcandpackages/rest/src(25) is a docstring or comment — no domain handler calls it. The two HTTP install sites (POST /packages,protocol.installPackage) callSchemaRegistry.installPackage, which never callsdefineStack.Why ONE code and not five
There is exactly one raise site:
validateCrossReferencesreturns every finding as astring[]anddefineStackthrows the collected set at once, so a single refusal can carry findings from several classes together — a per-class code would have to pick one of several true answers. The classes stay machine-readable inissues.The set is also wider than "undefined object": the same aggregate carries the duplicate-action-key, global-
update-action and mappingjavascript-transform findings. The card suggestedSTACK_CROSS_REFERENCE_UNDEFINED_OBJECT; that spelling would be false for those three, so the code names the rule FAMILY.Deliberately not exported
packages/spec/src/index.tsre-exports this module withexport *, so exporting the class would widen the published api-surface of the contract package. The ADR-0112 contract is thecode/statusfields, which every reader —resolveThrownHttpErrorand this repo's rejection pins alike — reads structurally, not byinstanceof. Export it the day a consumer needs the narrowed type, as its own change.Red then green
The test was written first and seen failing, against
origin/maind4c2cb196withpackages/specfreshly built.The 7 passing in the red run are the message-text pins: they prove each of the six fixtures reaches the cross-reference gate for the right reason before the envelope assertions are read. The pins assert the ENVELOPE (
code,status), nevertoThrow()alone.Verification — union re-run on final head
24af8e52a, exit codes captured before any pipepnpm --filter @objectstack/spec testpnpm --filter @objectstack/objectql exec vitest run src/registry-cross-package-item-classes.test.tspnpm --filter @objectstack/spec check:generatedcheck:api-surfaceincludedpnpm --filter @objectstack/{spec,runtime,objectql} typecheckcheck:dispatcher-error-vocabularyunclassified-site ... stamps unregistered code)check:error-code-casing·check:error-status-conformance·check:nul-bytes·check:doc-authoringcheck:cross-package-test-inputs·check:test-source-alias·check:changeset-gate-self-testscheck:merge-driver·check:spec-parsed-alias·check:published-files·check:partof-closing-keywordGate family derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(73 families) rather than from a hand-written list.Two readings worth recording.
@objectstack/runtime typecheckfirst returned exit 2 with 18TS2307 Cannot find module— zero in any file this PR touches; it was the unbuilt-dependency-closure row of AGENTS.md §9, and it is exit 0 afterpnpm --filter '@objectstack/runtime^...' build. Andpackages/spec'stsc --noEmitdoes not read*.test.ts(0 hits under--listFiles); the new test's coverage comes fromtsconfig.test.jsonviacheck:test-typecheck, where it is present and contributes 0 of the package's 261 pinned debt errors.The envelope-absence pin
packages/objectql/src/registry-cross-package-item-classes.test.tspinned the envelope's ABSENCE, with a header instructing that a red there means an envelope arrived and the pin should be updated, never deleted. Done:ENVELOPE ABSENCEbecomesENVELOPE PRESENCE, assertingcodeandstatus, and the header section is rewritten to record the envelope as present.Generated by Claude Code