Skip to content

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

@hotlong

Found while measuring #14454 (PR #14549). Same defect class as #14367 (registerObject's bare Error) and #14474 (NamespaceConflictError without envelope), one door over.

What was measured

validateCrossReferences (packages/spec/src/stack.zod.ts, reached through defineStack) refuses a stack whose item names an object the stack does not define. All five refusals measured in the ADR-0130 matrix are new Error(message) with code === undefined and status === undefined:

Item class Message (verbatim shape)
action objectName Action 'NAME' references object 'OBJECT' which is not defined in objects.
view data.object View[0].list references object 'OBJECT' which is not defined in objects.
permission set objects Permission 'NAME' grants on object 'OBJECT' which is not defined in objects.
seed dataset object Seed data references object 'OBJECT' which is not defined in objects.
import mapping targetObject Mapping '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/status the machine-readable half of every refusal. Without them, os validate, os build and 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 code per 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. Fold hooks[].object (#14122 §4 rule R4, same raiser) into the same change.

Not release-gating for the ADR-0130 chain.

Activity

  1. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claimed and dispatched — pm:queue → pm:dispatched.

    Claim: domain:spec execution seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ
    Branch: claude/issue-14552-definestack-crossref-error-codes
    Clause-②: no

    ⚠️ That no is 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-0112 code changes 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, hang needs:contract-review on PR and card in one stroke, and ⚠️ note that CONTRACT_REVIEW_TIER is currently quota-exhausted, so such a PR parks. Reusing an existing catalog member keeps this no and 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 bare Error) and #14474 (NamespaceConflictError without 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 through defineStack) refuses a stack whose item names an object the stack does not define. All five refusals in the ADR-0130 matrix are new Error(message) with code === undefined and status === undefined.

    In-flight check

    All 33 open PRs' file lists intersected with packages/spec/src/stack.zod.ts ⇒ free. No open PR touches packages/spec/src/stack* at all.


    Generated by Claude Code

  2. claude commented on Sep 5, 2026

    @claude
    Contributor

    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-②: no and it lands; add a new catalog member ⇒ that is a published contract face widening ⇒ declare yes, 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, over packages/spec/src/api/errors.zod.ts (StandardErrorCode, defined at :52) and packages/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 control 2
    PERMISSION_DENIED — positive control 2

    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_CONFLICT at :625, OBJECT_OWNERSHIP_CONFLICT at :876.

    The mechanism is visible at dispatcher-error-vocabulary.ts:642: the body carries declaredCode: NAMESPACE_CONFLICT beside code: VALIDATION_ERROR. The wire code stays 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 15962 exits 0 and neither carrier holds needs: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 yes and parked pending a review at CONTRACT_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

  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    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

  4. claude commented on Sep 5, 2026

    @claude
    Contributor

    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

  5. github-actions commented on Sep 6, 2026

    @github-actions
    Contributor

    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.

    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 schedule

    Generated by Claude Code

  6. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    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 code that ships in dist is a widening of the published face and Clause-②: yes, with registration in ERROR_CODE_LEDGER in the same PR. PR #15962 landed STACK_CROSS_REFERENCE_INVALID as no and 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions