Skip to content

fix(spec): defineStack's cross-reference refusal carries an ADR-0112 envelope - #15962

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-14552-definestack-crossref-error-codes
Sep 5, 2026
Merged

fix(spec): defineStack's cross-reference refusal carries an ADR-0112 envelope#15962
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-14552-definestack-crossref-error-codes

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #14552

validateCrossReferences — reached through defineStack — refuses a stack whose items name an object the stack does not define. That refusal was new Error(message) with code and status both undefined, so all five REFUSED item classes of the ADR-0130 matrix plus the hooks[].object rule (#14122 §4 rule R4) were distinguishable only by MESSAGE TEXT. It now throws StackCrossReferenceError: code: 'STACK_CROSS_REFERENCE_INVALID', status: 422, one entry per finding in issues, 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:

code in StandardErrorCode in ERROR_CODE_LEDGER
OBJECT_OWNERSHIP_CONFLICT (#14367) no no
NAMESPACE_CONFLICT (#14474) no no
STACK_CROSS_REFERENCE_INVALID (this PR) no no

Neither precedent registered its code. ErrorCode = StandardErrorCodeERROR_CODE_LEDGER is 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 in packages/runtime/src/dispatcher-error-vocabulary.ts, which pnpm check:dispatcher-error-vocabulary enforces both ways.

So this PR adds no catalog member, and check:api-surface is green with nothing regenerated. ⇒ Clause-②: no, re-declared from the delivered diff.

Reusing an existing member was considered and rejected on accuracy: INVALID_REFERENCE is documented as a foreign-key reference at the data layer and OBJECT_NOT_FOUND as a runtime 404 lookup; neither names an authoring-time dangling declaration.

Reachability: measured, not inferred

door: 'none', verdict: 'boot-refusal'. defineStack is an authoring/boot-time entry point. Every non-test occurrence of defineStack under packages/runtime/src and packages/rest/src (25) is a docstring or comment — no domain handler calls it. The two HTTP install sites (POST /packages, protocol.installPackage) call SchemaRegistry.installPackage, which never calls defineStack.

Why ONE code and not five

There is exactly one raise site: validateCrossReferences returns every finding as a string[] and defineStack throws 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 in issues.

The set is also wider than "undefined object": the same aggregate carries the duplicate-action-key, global-update-action and mapping javascript-transform findings. The card suggested STACK_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.ts re-exports this module with export *, so exporting the class would widen the published api-surface of the contract package. The ADR-0112 contract is the code / status fields, which every reader — resolveThrownHttpError and this repo's rejection pins alike — reads structurally, not by instanceof. 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/main d4c2cb196 with packages/spec freshly built.

RED    pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 \
         src/stack-cross-reference-envelope.test.ts
       EXIT=1 — Tests 13 failed | 7 passed (20)
       AssertionError: expected undefined to be 'STACK_CROSS_REFERENCE_INVALID'

GREEN  (same command, after the fix)
       EXIT=0 — Tests 20 passed (20)

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), never toThrow() alone.

Verification — union re-run on final head 24af8e52a, exit codes captured before any pipe

command exit
pnpm --filter @objectstack/spec test 0 — 477 files / 12807 tests
pnpm --filter @objectstack/objectql exec vitest run src/registry-cross-package-item-classes.test.ts 0 — 14 tests
pnpm --filter @objectstack/spec check:generated 0 — all 15 artifacts current, check:api-surface included
pnpm --filter @objectstack/{spec,runtime,objectql} typecheck 0, 0, 0
check:dispatcher-error-vocabulary 0 (red before the row: unclassified-site ... stamps unregistered code)
check:error-code-casing · check:error-status-conformance · check:nul-bytes · check:doc-authoring 0
check:cross-package-test-inputs · check:test-source-alias · check:changeset-gate-self-tests 0
check:merge-driver · check:spec-parsed-alias · check:published-files · check:partof-closing-keyword 0

Gate 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 typecheck first returned exit 2 with 18 TS2307 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 after pnpm --filter '@objectstack/runtime^...' build. And packages/spec's tsc --noEmit does not read *.test.ts (0 hits under --listFiles); the new test's coverage comes from tsconfig.test.json via check: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.ts pinned 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 ABSENCE becomes ENVELOPE PRESENCE, asserting code and status, and the header section is rewritten to record the envelope as present.


Generated by Claude Code

…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
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 1 anchor(s) matched too much of the corpus to be a work list: defineStack (symbol, 59 pages)
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 135 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3f89967e35acd69cbd9be7310fdb6ee6d83ca93epackageMentionDocs.

Which tree this was computed on

This run read content/docs from e81fcba713174fb5e4504a77c2b976caee6216b6 — the merge of head 24af8e52a937c5e3ee23c5b8f1d2adb4082a220a into base 3f89967e35acd69cbd9be7310fdb6ee6d83ca93e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Docs Drift Check close-out — a NULL result, stated as a measurement

Addendum to the report above, at PR head 24af8e52a. The drift check reported "nothing to list" while naming two blind spots that both apply to this diff — defineStack dropped as an anchor (59 pages, too broad), and its structural limit that an emitter-only diff cannot list a page stating the rule by its INPUTS. This diff is emitter-only, so I read the corpus independently rather than inheriting that null.

⛔ 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.

Method

Probes 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 (throws an Error, bare Error, throws a plain Error, no code, code and status, err.code, error.code); and the input-side phrasings (must name an object, object the stack defines, defined in objects, undeclared object, dangling reference, not defined in).

Verdict: nothing published is falsified

The decisive negative. throws an Error, throws a plain Error and bare Error return zero hits across all of content/docs/**. No page states this refusal's shape, so no page claims it has no code / status.

Pages read per-file, and why each stands:

Page What it says Still true?
data-modeling/import-mappings.mdx Publishes the mapping refusal's message verbatim: "the build fails with Mapping '...' targets object '...' which is not defined in objects." Yes — message is byte-for-byte unchanged. Makes no claim about Error / code / status.
ui/reports.mdx Publishes the sibling app-to-report message from the same aggregate Yes — same reason, message unchanged
api/error-catalog.mdx Generated from the spec ledger. No defineStack, no cross-reference, no STACK_ code Yes — consistent with the code not being ledger-registered
protocol/kernel/error-handling.mdx Publishes status per code. Same three greps: zero hits Yes
ui/views.mdx "os validate fails the build" — but about searchableFields lint diagnostics, a different subsystem with kebab-case codes Yes — unrelated
protocol/objectql/schema.mdx "not defined in .object.yml" — YAML system fields Yes — unrelated

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.

content/docs/releases/**: the single hit (v17.mdx, "grants on object") is about sharing-gate grants, not this refusal. Nothing there is falsified, so there is nothing for me to report for separate filing — and I did not edit that tree.

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 STACK_CROSS_REFERENCE_INVALID would also be wrong in api/error-catalog.mdx specifically — that page is generated from the ADR-0112 ledger, and this code is deliberately unregistered because no wire door raises it.

One incidental corroboration from the read: api/error-catalog.mdx documents INVALID_REFERENCE as "Reserved for an invalid foreign-key reference. No route emits it today." That independently confirms rejecting it as a reuse candidate for an authoring-time dangling declaration.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 5, 2026 16:59
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit f7db8f4 Sep 5, 2026
36 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-14552-definestack-crossref-error-codes branch September 5, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants