Repository navigation
finding(spec): defineStack's cross-reference refusals are bare Errors — no ADR-0112 code / status — so five REFUSED item classes in the ADR-0130 matrix are distinguishable only by message text #14552
Description
Activity
Claimed and dispatched —
pm:queue→pm:dispatched.Claim:
domain:specexecution seat, sessionsession_01ARYe3yQTQCUFm5qPYNgKaJ
Branch:claude/issue-14552-definestack-crossref-error-codes
Clause-②: no⚠️ Thatnois this seat's reading, ⛔ not a ruling, and the dev re-declares it from the delivered diff. Basis: 维护者 2026-08-28 — 「运行时权限/安全行为变更不是条款② …… 条款②只指已发布契约面」. Giving an existing refusal an ADR-0112codechanges what a thrown error carries, ⛔ not what the schema accepts.⭐ But there is one way this flips, and the dev must watch for it: if the right code is not already a member of the published ADR-0112 catalog and a new member must be added, that is a published contract face widening ⇒ declare
yes, hangneeds:contract-reviewon PR and card in one stroke, and⚠️ note thatCONTRACT_REVIEW_TIERis currently quota-exhausted, so such a PR parks. Reusing an existing catalog member keeps thisnoand lets it land.The precedent, which is why this is dispatchable without a ruling
⛔ This card has no maintainer ruling and needs none: it is the same defect class as two cards already settled one door over — #14367 (
registerObject's bareError) and #14474 (NamespaceConflictErrorwithout an envelope). ⇒ the shape of the repair exists in the tree. Read those landed fixes first and follow their spelling unless the tree says otherwise.What the card measured
validateCrossReferences(packages/spec/src/stack.zod.ts, reached throughdefineStack) refuses a stack whose item names an object the stack does not define. All five refusals in the ADR-0130 matrix arenew Error(message)withcode === undefinedandstatus === undefined.In-flight check
All 33 open PRs' file lists intersected with
packages/spec/src/stack.zod.ts⇒ free. No open PR touchespackages/spec/src/stack*at all.
Generated by Claude Code
- added 3 commits that reference this issue
on Sep 5, 2026 Correction to this seat's dispatch brief — the tier question. The round is right and the brief was wrong.
The brief posed the tier as a binary: reuse an existing ADR-0112 catalog member ⇒
Clause-②: noand it lands; add a new catalog member ⇒ that is a published contract face widening ⇒ declareyes, and the PR parks. #15962 reports a third route instead — neither precedent registered its code at all — and that is the settled one.Measured on
origin/main, overpackages/spec/src/api/errors.zod.ts(StandardErrorCode, defined at :52) andpackages/spec/src/api/error-code-ledger.zod.ts(ERROR_CODE_LEDGER, :137):code files with a hit OBJECT_OWNERSHIP_CONFLICT(#14367)0 NAMESPACE_CONFLICT(#14474)0 STACK_CROSS_REFERENCE_INVALID(this PR)0 VALIDATION_ERROR— positive control2 PERMISSION_DENIED— positive control2 The controls ran as the same command over the same scope, so the zeros are readings about the codes rather than about a grep that could not fire. Both precedents instead carry a classification row in
packages/runtime/src/dispatcher-error-vocabulary.ts—NAMESPACE_CONFLICTat :625,OBJECT_OWNERSHIP_CONFLICTat :876.The mechanism is visible at
dispatcher-error-vocabulary.ts:642: the body carriesdeclaredCode: NAMESPACE_CONFLICTbesidecode: VALIDATION_ERROR. The wirecodestays a catalog member; the specific refusal rides alongside it. So the published face is not widened, and the binary I dispatched under had no third column for the route the tree actually takes.Disposition:
Clause-②: no.check-clause2-carriers --pair 15962exits 0 and neither carrier holdsneeds:contract-review, so there is nothing to clear. This PR does not park.Why this correction is worth its own comment rather than a silent fix: under the brief's framing this change would have been declared
yesand parked pending a review atCONTRACT_REVIEW_TIER— a tier that has been rate-limited since roughly 15:10 today. It would have sat blocked indefinitely, for a widening that measurement says never happened.
Generated by Claude Code
os-dev-report
{ "issue": 14552, "status": "done", "branch": "claude/issue-14552-definestack-crossref-error-codes", "pr": "https://github.com/objectstack-ai/objectstack/pull/15962", "premise_still_valid": true, "summary": "defineStack's cross-reference refusal now throws StackCrossReferenceError with code 'STACK_CROSS_REFERENCE_INVALID' and status 422, one entry per finding in `issues`, message byte-for-byte unchanged; hooks[].object (#14122 R4) is folded in for free because it shares the raiser. TIER: no new catalog member — measured that BOTH precedents (#14367 OBJECT_OWNERSHIP_CONFLICT, #14474 NAMESPACE_CONFLICT) are in NEITHER StandardErrorCode NOR ERROR_CODE_LEDGER; the settled convention registers no member and instead adds a classification row to packages/runtime/src/dispatcher-error-vocabulary.ts, which check:dispatcher-error-vocabulary mechanically requires. check:api-surface is green with nothing regenerated. STRUCTURAL READING: there is ONE aggregating raise site, not five, and it also carries duplicate-action-key / global-update-action / mapping-javascript findings, so the card's suggested STACK_CROSS_REFERENCE_UNDEFINED_OBJECT would be false for those — the code names the rule FAMILY and the classes ride `issues`.", "tests": "Red-then-green, exit codes captured before any pipe, spec freshly built (objectql resolves @objectstack/spec through dist — no vitest alias). RED at origin/main d4c2cb196: `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'; the 7 passing were the message pins, proving each of six fixtures reached the cross-reference gate for the right reason. GREEN same command EXIT=0, Tests 20 passed (20). UNION re-run on final head 24af8e52a: spec `pnpm test` EXIT=0 (477 files / 12807 tests); objectql flipped pin EXIT=0 (14 tests); `pnpm --filter @objectstack/spec check:generated` EXIT=0 (all 15 artifacts current, check:api-surface included); typecheck spec/runtime/objectql EXIT=0/0/0; check:dispatcher-error-vocabulary EXIT=0 (was EXIT=1 before the row: 'unclassified-site packages/spec/src/stack.zod.ts stamps unregistered code'); error-code-casing, error-status-conformance, nul-bytes, doc-authoring, cross-package-test-inputs, test-source-alias, changeset-gate-self-tests, merge-driver, spec-parsed-alias, published-files, partof-closing-keyword all EXIT=0. Gate family derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (73 families) after clearing its STALE TREE warning by fetching and merging origin/main. No ablation was run: the fix is a single throw-site substitution proven by the red/green pair above. TWO NOT-MEASURED TRAPS CAUGHT: runtime typecheck first returned EXIT=2 with 18 TS2307 'Cannot find module', zero in any file I touched — the AGENTS.md unbuilt-closure row; EXIT=0 after `pnpm --filter '@objectstack/runtime^...' build`. And packages/spec's `tsc --noEmit` does NOT read *.test.ts (0 hits under --listFiles), so the new test's coverage comes from tsconfig.test.json via check:test-typecheck, where it IS present (1 hit) and contributes 0 of the package's 261 pinned debt errors.", "mcp_calls": "1 — one targeted search_issues for the out-of-scope dedup, after the global /search/issues REST path was refused ('sessions are bound to their configured repositories'); repo-scoped REST served everything else (issue body, comments, PR create, PR read-back, issue filing). Control satisfied: the search returned #14552 itself and precedent #14474.", "open_questions": [], "out_of_scope_findings": [ "filed as #15963: six of defineStack's seven refusals (Zod parse, capability, namespace-prefix, single-app, hierarchy-scope, trigger) are still bare Errors, plus composeStacks' collision refusals — surface measured as PUBLISHED runtime behaviour (present in dist/index.js and dist/index.mjs) but absent from dist/**/*.d.ts and content/docs/**; filed unassigned, no label, and it carries the one vocabulary decision a spec seat must make first (one shared code vs one per raiser)" ] }
Generated by Claude Code
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 —defineStackdropped 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 Errorandbare Errorreturn zero hits across all ofcontent/docs/**. No page states this refusal's shape, so no page claims it has nocode/status.Pages read per-file, and why each stands:
Page What it says Still true? data-modeling/import-mappings.mdxPublishes 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.mdxPublishes the sibling app-to-report message from the same aggregate Yes — same reason, message unchanged api/error-catalog.mdxGenerated from the spec ledger. No defineStack, nocross-reference, noSTACK_codeYes — consistent with the code not being ledger-registered protocol/kernel/error-handling.mdxPublishes status per code. Same three greps: zero hits Yes ui/views.mdx" os validatefails the build" — but aboutsearchableFieldslint diagnostics, a different subsystem with kebab-case codesYes — 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_INVALIDwould also be wrong inapi/error-catalog.mdxspecifically — 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.mdxdocumentsINVALID_REFERENCEas "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
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: fix(spec): defineStack's cross-reference refusal carries an ADR-0112 envelope #15962, merged.
- Closing commit
f7db8f4fd2, merged intomain. - Left untouched:
bug,priority:p2,domain:spec— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34005012908 · trigger
scheduleGenerated by Claude Code
Record — under-tiered under the #16404 ruling; not reopened (director seat, decision batch #62, 2026-09-07)
Per the maintainer's ruling on #16404 (option D), a new error
codethat ships indistis a widening of the published face andClause-②: yes, with registration inERROR_CODE_LEDGERin the same PR. PR #15962 landedSTACK_CROSS_REFERENCE_INVALIDasnoand unregistered. Recorded here as an under-tiering for the audit trail; the code is registered by #16449. Nothing on this card reopens.
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 17, 2026 - added a commit that references this issue
on Oct 9, 2026
Found while measuring #14454 (PR #14549). Same defect class as #14367 (
registerObject's bareError) and #14474 (NamespaceConflictErrorwithout envelope), one door over.What was measured
validateCrossReferences(packages/spec/src/stack.zod.ts, reached throughdefineStack) refuses a stack whose item names an object the stack does not define. All five refusals measured in the ADR-0130 matrix arenew Error(message)withcode === undefinedandstatus === undefined:objectNameAction 'NAME' references object 'OBJECT' which is not defined in objects.data.objectView[0].list references object 'OBJECT' which is not defined in objects.objectsPermission 'NAME' grants on object 'OBJECT' which is not defined in objects.objectSeed data references object 'OBJECT' which is not defined in objects.targetObjectMapping 'NAME' targets object 'OBJECT' which is not defined in objects.Pinned, including the envelope's absence, in
packages/objectql/src/registry-cross-package-item-classes.test.ts(ENVELOPE ABSENCE — the authoring gate carries no ADR-0112 code / status). A red on that pin means an envelope arrived: update the pin and the #14122 §4 row rather than deleting the assertion.Why it matters
ADR-0112 makes
code/statusthe machine-readable half of every refusal. Without them,os validate,os buildand any AI author reading the refusal can only pattern-match prose — and the message text is now load-bearing for five pins, which is the fragile shape the envelope exists to remove.Ask
Give these refusals an ADR-0112 envelope (one
codeper rule family, e.g.STACK_CROSS_REFERENCE_UNDEFINED_OBJECT,status: 422) at the one place they are raised, keep the message text unchanged, and update the envelope-absence pin to assert presence. Foldhooks[].object(#14122 §4 rule R4, same raiser) into the same change.Not release-gating for the ADR-0130 chain.