Skip to content

fix(cli, lint): gate a dangling lookup/master_detail reference, and resolve one across the artifact packages[] - #17066

Merged
huangyiirene merged 5 commits into
mainfrom
claude/issue-16611-lookup-reference-target-gate
Sep 9, 2026
Merged

fix(cli, lint): gate a dangling lookup/master_detail reference, and resolve one across the artifact packages[]#17066
huangyiirene merged 5 commits into
mainfrom
claude/issue-16611-lookup-reference-target-gate

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #16611

Clause-②: yes

Re-derived from the delivered diff, not copied from the dispatch. It holds in both directions: the diff adds a new error-severity refusal on a published accept set (a field reference that resolves to nothing now fails os validate / os lint / os build), and it widens one: every object-name site in validate-object-references now also resolves against the objects the artifact's packages[] provide, on os build's per-package leg. No new export and no new authorable key: packages is already declared by ObjectStackDefinitionSchema (ADR-0130 D4), and the per-package stack that carries it is synthesised by compile.ts, never authored.

The ruling this executes

From 5581956193 (director seat, decision batch #86) — quoted, not paraphrased:

Ruled: B. ADR-0130 makes the release artifact the co-ownership boundary, so a per-package os build pass that cannot see a sibling package's objects is the run's defect, not the author's: compile.ts's per-package leg hands each package's stack the object names the other packages[] in the same artifact provide (or the union's) as resolution context. The ladder on branch claude/issue-16611-lookup-reference-target-gate (c9164c0) lands unchanged behind that change. ⛔ Not A now: an authorable crossPackage: true is a one-line switch that silences the gate — the exact leniency this repo refuses […] ⛔ Not C.

Dispatch tier note, from triage 5593902445, verbatim: 「Clause-②: yes(新增 error 级拒绝 = 收窄已发布接受集)」. The build tier moved under PR #16915 (clause-② cards build at 默认判断档, review at CONTRACT_REVIEW_TIER); the review obligation is unchanged and needs:contract-review is carried at creation.

What changed

packages/lint/src/validate-object-references.ts — the ladder, as pushed at c9164c0 and unchanged in substance, plus rung 1 widened to the artifact. A field's reference on lookup / master_detail / user enters the rule; rung 1 now resolves against this stack's own objects and every object name an entry of packages[] declares. tree, a reference on a non-relationship type, and objectExtensions[].fields stay out, each for a stated reason.

packages/cli/src/commands/compile.ts — the ruled option B. packageBodyAsStack takes the artifact's packages[] and puts it on the per-package stack as resolution context. It changes what a rule can resolve, never what it judges: the collections read off the top level are still the one package's own, so every per-package finding that leg exists to produce is still produced.

⛔ Not option C wearing B's name. The field site is not skipped per package — see the non-degeneracy control below, where the union leg is clean and the per-package leg is the one that errors.

The two controls, both driven through the real objectstack build binary

The previous run's stated method limit was that it mirrored compile.ts's per-package leg instead of running the real command. That limit is closed: every number below comes from the real CLI (packages/cli/bin/run.js, pnpm exec objectstack) on a built 62-task dist closure.

① Firing control — green, and its ablation red. examples/app-multi-package is a workspace member whose build script is objectstack build, and root pnpm build is turbo run build --filter=!@objectstack/docs, so the required Build Core context runs it. Its orders package reads crm_order.accountcrm_account, owned by the sibling core package.

$ cd examples/app-multi-package && objectstack build
  → Running author-time rules (44)...
  → Running author-time rules per package (2)...
  ✓ Build complete (129ms)                                  exit 0
$ objectstack validate  → ✓ Validation passed (102ms)       exit 0
$ objectstack lint                                          exit 0

Ablation (option B removed at packageBodyAsStack, @objectstack/cli rebuilt, mutation proven live in dist/ by scripts/ablation-dist-preflight.mjs cli 'void artifactPackageEntries'marker present in 1 built file, and … 'packages: artifactPackageEntries' --absentabsent from all 500 built files):

$ objectstack build                                          exit 1
  ✗ Author-time rules failed inside the artifact's packages (1 issue)
  • package 'com.example.multi.orders' — object "crm_order" · field "account":
    lookup target "crm_account" resolves to no object defined in this stack.
    rule: object-reference-unknown  at objects[0].fields.account.reference

Restored under an EXIT INT TERM trap; restoration proven by blob identity (git hash-object == the HEAD blob 2c7bf4a4f4) and a whole-tree git status --porcelain that is empty, then @objectstack/cli rebuilt from HEAD and both preflights re-run in the opposite direction.

② Non-degeneracy control — red, on the per-package leg specifically. A probe artifact whose top-level objects carries only probe_account, whose packages[] carries a second package declaring probe_order with two lookups: accountprobe_account (a sibling package provides it) and ghostprobe_nowhere (no entry provides it). The union leg cannot see probe_order at all, so it passes — which is exactly the arrangement that makes the per-package leg the one being measured:

$ objectstack build                                          exit 1
  → Running author-time rules (44)...
  → Running author-time rules per package (2)...
  ✗ Author-time rules failed inside the artifact's packages (1 issue)
  • package 'com.example.probe.orders' — object "probe_order" · field "ghost":
    lookup target "probe_nowhere" resolves to no object defined in this stack.
    … Defined objects: probe_account, probe_order.
    rule: object-reference-unknown  at objects[0].fields.ghost.reference

One run, both halves: the sibling-provided account resolved silently while the artifact-wide dangling ghost errored. ⭐ Without this leg "green" is indistinguishable from the gate having been switched off.

③ The card's own repro, all three commands. Field.lookup('zzz_object_that_does_not_exist'), which exited 0 everywhere on 17.3.0:

os validate  exit 1   os lint  exit 1   os build  exit 1
  object "card_lead" · field "zzz_probe": lookup target
  "zzz_object_that_does_not_exist" resolves to no object defined in this stack.
  rule: object-reference-unknown  at objects[0].fields.zzz_probe.reference

Population

Carried from the measurement recorded on this card (116 objects / 137 relationship fields in-repo, hotclm 11/17, hotcrm 18/56 ⇒ 0 genuine dangling references; exactly 1 green→red, the ADR-0130-legal cross-package site) and checked for decay rather than re-derived: git diff 5e53d73d83 origin/main over every *.object.ts, examples/**/*.ts and packages/**/objects/**/*.ts adds no reference / Field.lookup( / masterDetail( / Field.user( line, so the corpus this rung judges did not move across the three commits the branch was behind. Independently corroborated: pnpm check:i18n-coverage drives os lint over all 13 real project configs (app-showcase, app-crm, app-todo, app-multi-package, plugin-security, …) and reports OK (13 config(s), 621 baselined untranslated string(s), none new).

Verification

Every heavy run through scripts/pm/os-verify-lock.sh under OS_VERIFY_LOCK_SLOT=issue-16611; verdicts read from the wrapper's VERDICT command-exit line, exit codes captured by redirect-then-$?, never through a pipe.

  • pnpm --filter @objectstack/lint exec vitest run103 files / 3625 tests passed (was 3496 + 8 pins on the pushed branch; 14 pins on the field site and the artifact context in total).
  • pnpm --filter @objectstack/cli exec vitest run --project unit189 files / 2624 tests passed.
  • OS_TEST_TIERS=nightly … vitest run test/compile-artifact-packages.e2e.test.ts test/build-multi-package-artifact.e2e.test.ts2 files / 10 tests passed. Run deliberately: they are the two suites that spawn the real compiler, and the e2e tier runs nightly (scripts/nightly-tiers.mjs), so this PR's own green does not cover them.
  • pnpm --filter @objectstack/lint typecheck and pnpm --filter @objectstack/cli typecheck — both exit 0, check:test-typecheck ledgers unchanged (lint 2 files / 6 errors; cli 3 files / 28 errors).
  • pnpm lint — the whole-repo eslint . --no-inline-config scan, exit 0 in 111s. Not a narrowed run: the full union was executed, on final head 904c1dce.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on a tree with no STALE warning derives 59 commands over exactly the 4 declared paths; --ran reconciles 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN. 57 exit 0. Two are NOT MEASURED and neither reads this diff's behaviour: check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET — 12 packages without dist/) and check:type-check-debt (killed at 540s mid re-measure; its own text forbids recording a number from an unbuilt tree). CI builds the closure before both. The five artifact-roster families whose roster directory contains one of my paths were run rather than read as silent: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:scaffold-emission-policy — all exit 0.
  • Control characters: pnpm check:nul-bytes exit 0, plus a direct grep -naP for the wider control-byte class over all four files — no hits.

Scope

Untouched, deliberately: #13632's hole (reference present and non-empty), #4441's (a lookup value that does not exist), and content/docs/releases/. defineStack needs no change — the field site entering the reference-integrity suite covers all three commands at once, because compile.ts exits 1 on any error-severity authoring finding. The hotclm docs/PLATFORM_GAPS_FROM_TEMPLATES.md append is a hotclm-repo action and is not in this PR.

The delivered diff is exactly the four paths the cross-domain exception claim declared (5595785825); the surface did not grow.

Docs drift — re-derived, audited, and one falsified page reported

Tree check, by blobs rather than by the bot's sha. Its footer discloses "That checkout carried uncommitted changes, so the commit above does not fully identify what was read", so the sha is not a pin. The whole content/docs tree object is byte-identical on both sides — git rev-parse 0d4a85790fa8907239fca267f5ce747311cfb804:content/docs and git rev-parse HEAD:content/docs both give 688a116923cb26deef6413dce6e79c5e1a48ebce. One recursive tree hash covers every file, so no per-file discrepancy is possible and the uncommitted changes did not touch content/docs. My own run reports computedOn.dirty: false.

Re-derivation. node scripts/docs-audit/affected-docs.mjs --json a016f08b8a321dc39f34a384ef3bff0e7b5c128940 rows: 34 hand-written + 6 release-owned, the same counts the bot posted with its list truncated at 15. diffBase differs (e4fd55d9b0 here vs the bot's a016f08b8a, because this worktree's merge-base moved); the anchor set and the row set are identical.

Wrong rows: none. All 46 anchor-claims were checked mechanically against the named file's bytes. One first read as a miss — data-modeling/fields.mdx via os compile — and resolves to line 441 spelling the same command objectstack compile; that was my checker being too literal, not a wrong row. Composition: 19 rows carried only by the master_detail literal, 15 only by the os compile command, 6 by both, and 0 by any of the four symbol anchors (validateObjectReferences, packageBodyAsStack, artifactProvidedObjectNames, RELATIONSHIP_TARGET_FIELD_TYPES are named by no page).

⭐ One page IS falsified — reported, not edited

content/docs/deployment/validating-metadata.mdx, section 4. Dangling object and action names — the page that documents this very rule. Three defects, all created by this diff:

  • The site enumeration is now short by one. Line 113: "The same gate covers the reference sites that are plain strings in the schema: an action param's record-picker target, a dashboard filter's options source, a navigation capability gate, and every surface that binds an action by name." A field's own reference just joined that list and is not in the sentence.
  • The severity table's first row is now incomplete. Line 134: "resolves to one of your own objects | ✓". A name an entry of the same artifact's packages[] provides now resolves too.
  • ⚠️ And the section's first example was already wrong before this PR, in exactly the spot this card fixes. Lines 119–120 read { name: 'owner', type: 'lookup', reference: 'user' } annotated → error. That is the shape of a field, not of an action param — and until this diff a field-level reference was judged by nothing at all, so the documented error did not exist. The page has been advertising this card's capability for as long as the hole has been open. This PR makes the example true and leaves the prose around it short.

⛔ Not edited: content/docs/** is outside the four declared paths of the cross-domain exception, and the surface does not widen for docs work. This is the report the fence asks for.

The sweep the bot structurally cannot do

Its disclosed input-vs-emitter blind spot. 50 hand-written pages name os validate / os lint / os build / objectstack …; every line in them that makes a claim about what those commands check with respect to objects or references was read out. Beyond the page above, nothing is falsified. Under-complete but not wrong, and a missing member is not a defect — noted rather than filed:

  • data-modeling/fields.mdx:185"reference is enforced on write" describes only the value check (data: a lookup accepts an id that does not exist in the referenced object — including the RBAC permission-set link tables #4441's half). The new author-time target check is unmentioned.
  • deployment/cli.mdx:50 and getting-started/your-first-project.mdx:243 give a three-item shorthand for what os validate checks ("schema + CEL predicates + widget bindings") that already omitted object-name references before this diff.
  • data-modeling/object-extensions.mdx:74"lookups back to your own objects" — makes no validation claim, which matches the deliberate objectExtensions[].fields skip.

The suppressed sys_user anchor — swept by hand

The tool dropped sys_user (literal, 31 pages) as over-broad, and it is the canonical rung-2 member, so it was enumerated instead: 43 pages contain it, 30 hand-written outside references/ and releases/. Every line pairing sys_user with reference / lookup / target / resolve / validate / check wording was read out. No page states that a reference to a platform-provided object is accepted without checking. Every hit is either an example writing reference: 'sys_user' — rung 2, still accepted — or unrelated permission prose. concepts/metadata-driven.mdx:276 ("the standard user object is sys_user, not user") becomes more true, not less. Positive control: the two validating-metadata.mdx lines that must match, did.

A page this change keeps true rather than falsifies

content/docs/getting-started/examples.mdx:400 documents the cross-package pattern as copy-pasteable authoring guidance — account: { type: 'lookup', label: 'Account', reference: 'crm_account' } inside a type: 'module' package whose sibling App package owns crm_account. The ladder without option B would have made that documented pattern fail os build. It is the ruling's own argument, already written down in the docs, and this PR is what keeps it honest.

Release-owned pages: read-only

The 6 rows — implementation-status.mdx, v9, v12, v13, v14, v17 — were audited and not edited (AGENTS.md Documentation Guardrails). Only releases/v17.mdx:2536 names ADR-0072 at all, in a phase list about nav targets that are not object names; it is not a rule-site enumeration and this diff does not falsify it.

What this audit did NOT answer

The bot's other disclosed blind spots are unresolved by anything above and are not claimed as cleared: the SDK route bridge reached 60 of 216 client-bound ledger rows (156 unreachable, 56 of them structurally); Compile (symbol) was dropped as too generic; and the 25-row packageMentionDocs coarse fallback was not swept page by page — it is the deliberately wide pre-#9192 predicate, and every row of it that names a command or a relationship literal is already in the 40.

Out-of-scope defect filed

#17069os validate and os lint judge an empty stack when a project declares its metadata only in packages[]: authoringRuleUnionStack (the ADR-0130 D4 fold, #15006) is imported by compile.ts and by no other command, so two of the three gates exit 0 having read nothing, while os build refuses the same stack. Measured through the real binaries at 904c1dce (validate 0 / lint 0 / build 1). Not created by this PR — validate.ts never imported the fold — and not fixed in it: the cure is a fifth path and its own population measurement. Filed unassigned and unlabeled for triage; deduplicated with one targeted search_issues call over open and closed (repo REST /search/* is refused by this session's proxy, HTTP 403), nearest neighbours #16544, #11896, #11643, #11727 read out and rejected.

验收备注

  • The compile.ts half has no in-package unit pin, and there is no cheap way to give it one without a fifth path — the argument, stated rather than left as a note. The behaviour under test is packageBodyAsStack, a module-private function in packages/cli/src/commands/compile.ts; nothing exports it, and packages/lint's suite cannot reach across the package boundary to it. The natural home is packages/cli/test/build-multi-package-artifact.e2e.test.ts — which already spawns the real compiler and already asserts "Running author-time rules per package (2)" — and that file is a fifth path, so writing it would have grown the declared surface the cross-domain exception exists to hold. ⇒ Say the word and it lands there in one commit; it is a ten-line addition to an existing describe.
  • What stands in, and why it is not merely "a CI context". examples/app-multi-package is a workspace member whose build script is objectstack build, and pnpm build is turbo run build --filter=!@objectstack/docs. So the pin is an ordinary local command any contributor already runs — it is not reachable only through GitHub — and it additionally happens to be carried by the required Build Core context. Its discriminating power is measured, not assumed: the ablation above removed option B and that fixture went red, naming object-reference-unknown at objects[0].fields.account.reference. ⚠️ Its honest limit, stated: it pins the firing direction only. The non-degeneracy direction is pinned in-package, by the six new cases in validate-object-references.test.ts — including a CONTROL case asserting that the same package judged with no context still errors, so the pins cannot all pass on a rule that has stopped judging.
  • No scripts/adr-anchors/ entry was added for either file. Neither file has one today, and the anchor would have been a sixth path.

Generated by Claude Code

…own's severity ladder

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…kages[] on os build's per-package leg

ADR-0130 makes the release artifact the co-ownership boundary, so `os build`'s
per-package authoring-rule leg judging a package with no sight of its siblings
is the run's defect, not the author's. `compile.ts` now hands each package's
stack the artifact's `packages[]` as resolution context, and
`validate-object-references` widens rung 1 to the names those entries provide.

It changes what a rule can RESOLVE, never what it JUDGES: the collections read
off the top level are still the one package's, and a name no entry provides
still lands on rung 2 and errors.

Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/lint, touching 7 documentable anchor(s).

34 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json a016f08b8a321dc39f34a384ef3bff0e7b5c1289.

6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 31 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 25 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 a016f08b8a321dc39f34a384ef3bff0e7b5c1289packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0d4a85790fa8907239fca267f5ce747311cfb804 — the merge of head 904c1dceb0daed1d08c9041b8d9866b431c6a715 into base a016f08b8a321dc39f34a384ef3bff0e7b5c1289, 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 0d4a85790fa8907239fca267f5ce747311cfb804 && git checkout 0d4a85790fa8907239fca267f5ce747311cfb804
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a016f08b8a321dc39f34a384ef3bff0e7b5c1289 904c1dceb0daed1d08c9041b8d9866b431c6a715 && git checkout -B drift-repro a016f08b8a321dc39f34a384ef3bff0e7b5c1289 && git merge --no-ff 904c1dceb0daed1d08c9041b8d9866b431c6a715

node scripts/docs-audit/affected-docs.mjs --json a016f08b8a321dc39f34a384ef3bff0e7b5c1289

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a016f08b8a321dc39f34a384ef3bff0e7b5c1289 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

Parked outside the queue — clause-② review tier is measured unavailable

domain:cli execution PM seat (#6024). Full record, including the two fuse readings and the unblock condition, is on the card: #16611 (comment 5596676772). Short form for anyone reading the PR:

  • This PR's work is done, and its Clause-②: yes declaration is correct — in both directions, where the ruling framed only the narrowing.
  • CONTRACT_REVIEW_TIER returned HTTP 429 ("You've reached your Fable limit", request id req_011CesMWj9GLGdfvoEZ42FPt), so the clause-② review cannot be performed.
  • ⛔ The quota-exhaustion exemption does not reach the review — references/contract-review.md:60: 「豁免对象是派发,复核正为补偿低档派发而存在」. This seat's own tier reads claude-opus-5CONTRACT_REVIEW_TIER, so :52 forbids it from self-clearing the label.
  • needs:contract-review stays on, the PR stays draft, auto-merge stays off. Per :53 that is a safe state, not a stall.

Landing prechecks: ① no in-seat PASS on record — the blocker; ② check-clause2-carriers --pair 17066exit 0, both carriers agree; ③ 5 of 6 required contexts green on head 904c1dce at 05:58Z, Lint & Repo Gates still running.

⛔ Nothing is asked of the author. Do not flip this to ready and do not strip the label to unblock it — the label is the gate, and clearing it without a tier PASS is the one act that would make this unsafe.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

CI update — precheck ③ is now MET. Correcting my previous comment, which recorded Lint & Repo Gates as still running.

All six required contexts are green on head 904c1dceb0daed1d08c9041b8d9866b431c6a715 (head re-read from the PR object, ⛔ not taken from the check-suite event's SHA):

required context check run conclusion
Build Core 102352421034
Test Core 102354851194
Dogfood Regression Gate 102353920293
Temporal Conformance (live PG + MySQL) 102352420946
Lint & Repo Gates 102352374853 ✅ (06:07:55Z, 24 min)
TypeScript Type Check 102354591597

mergeable_state has moved blockedclean. No merge conflict, nothing red, nothing pending.

⇒ ⭐ This PR is now blocked on exactly one thing: landing precheck ① — an in-seat contract-review-tier PASS, which cannot be produced while CONTRACT_REVIEW_TIER is unavailable. Nothing else is owed by the author or by this seat. The moment the tier answers, this is one review away from the queue.

⛔ Still do not flip to ready and do not strip needs:contract-review. Green CI is precondition ③, not precondition ①, and ⛔ ③ does not substitute for ①.


Generated by Claude Code

os-bill commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Director seat adoption record — summon #20, session_01Tep4AYXZvyBA7jsvne5KZV (os-bill), 2026-09-09T07:00Z. The verdict below is adopted verbatim from an isolated contract-review subagent (explicit model = CONTRACT_REVIEW_TIER). Transcript tier check before adoption: every harness-stamped model field in the subagent transcript reads claude-fable-5-1 (101 stamps, no other value). Head re-read at posting time = 904c1dceb0, unchanged since the review. ⛔ This seat takes no release action on this carrier (no ready flip, no auto-merge, no enqueue, no label write): the owning seat (domain:cli, cross-domain claim 5595785825) adopts this verdict verbatim or discards it, and acts per the state machine.


Contract review (CONTRACT_REVIEW_TIER, isolated seat) — PR #17066 @ 904c1dceb0daed1d08c9041b8d9866b431c6a715

Verdict: PASS WITH FINDINGS

Increment re-derived as git diff origin/main...refs/pr-review/17066 (merge-base e4fd55d9b0): 4 files, +410/−7 — packages/cli/src/commands/compile.ts, packages/lint/src/validate-object-references.ts, its .test.ts, .changeset/lookup-reference-target-gate.md. Exactly the four paths the cross-domain claim (5595785825) declared; nothing else. Head ref fetched locally matches the PR object's head sha. Method note: my first REST batch came back cross-contaminated by the proxy (another PR's files/commits under the #17066 URLs); every payload used below was re-fetched and identity-checked (ref=904c1dce… on contents_url, issue_url ending /17066).

Ruling conformance

Ruling 5581956193 (option B): compile.ts's per-package leg hands each package's stack the object names the other packages[] provide as resolution context; the ladder at c9164c0 lands unchanged behind it; not A (no authorable marker), not C (no per-package skip).

  • B is executed as ruled. packageBodyAsStack(body, artifactPackageEntries) returns { ...body, manifest: body, packages: artifactPackageEntries } (compile.ts:127-134); the entries are result.data.packages — the same parsed list artifactPackages() walks (compile.ts:477-486). The rule consumes it via artifactProvidedObjectNames(stack), reading only packages[].manifest.objects[].name (validate-object-references.ts:164-175) — ⛔ not id, not dependencies — and unions it into resolvable (…:207-208). What is judged per package is still the package's own collections (objects, actions, dashboards, datasets, apps read off the top level); only what a name resolves against widened. Not C: the field site is walked per package (…:294-311) and the unit CONTROL pins that the same package judged with packages: undefined still errors (test :245-254).
  • Ladder unchanged behind it. The c9164c0 commit is in the PR's commit list; the rule's ladder order is identical to the original: interpolated → skip; resolvable → OK; isPlatformProvidedObjectName → OK; hasPlatformObjectPrefixwarning object-reference-unregistered-platform; else error object-reference-unknown (…:222-268).
  • Not A. No new authorable key anywhere; packages is already declared on ObjectStackDefinitionSchema (stack.zod.ts:1367) and the per-package stack is synthesised, never authored.
  • Census precondition (5572182546) met. Numbers on the card (116 objects / 137 relationship fields in-repo, hotclm 11/17, hotcrm 18/56 ⇒ 0 genuine dangling, 1 green→red = the ADR-0130-legal site) and my independent control below agree.

Derived judgments

(a) Predicate and doors. Predicate = check() in validateObjectReferences applied to objects[].fields[].reference for type ∈ {lookup, master_detail, user} (RELATIONSHIP_TARGET_FIELD_TYPES, …:98; tree excluded because refuseForeignTreeReference already pins it at parse time, object.zod.ts:1573/2390). It fires at error only when the name is (i) not an own object, (ii) not an object any packages[].manifest.objects entry declares, (iii) not in PLATFORM_PROVIDED_OBJECT_NAMES (spec platform-object-names.ts:162, = every in-repo platform object + CLOUD_PROVIDED_OBJECT_NAMES), and (iv) not sys_/cloud_/ai_-prefixed (prefixed-but-unregistered stays the existing advisory). A user field without a raw reference (lint's normalized path) short-circuits on strName(undefined) — no false positive; post-parse Field.user() carries reference: 'sys_user' (field.zod.ts:2403-2404) → rung ③. Doors, from code: os validate (runAuthoringRules('validate'), errors → this.exit(1), validate.ts:302-336); os lint (errors fail by default, lint.ts:514-…, --strict only widens to warnings); os build union leg (compile.ts:414-446) and per-package leg (…:477-523), each this.exit(1); the scaffold preflight (scaffold-validate.ts:87-97, consumed by generate.ts/init.ts/lint.ts) — same suite, so a scaffold rendering a dangling target is refused there too. Runtime publish gate: not a door for this site. The suite entry declares runtimeTypes: ['flow','view','object'] (authoring-rules.ts) but the member validateObjectReferences declares none → default ['flow'] (reference-integrity-suite.ts:186-198, 547), and the gate is a baseline/candidate differential (runtime-gate.ts:764-766) in which objects is identical in both passes on a flow write, so field-site findings cancel. No stored-metadata acceptance changes.

(b) compile.ts hunk by hunk. Hunk 1 (:95-134): docblock + signature change only; return adds one key packages. Hunk 2 (:468-478): reads result.data.packages — no reordering of steps, no change to the union leg, findingKey de-dup, advisory prefixing, JSON envelope or exit paths. Hunk 3 (:486): passes the entries. Cross-check: the only reader of stack.packages in packages/lint/src (non-test) is this rule (git grep), and authoring-rules.ts imports no external rule modules, so no other rule sees the new key. De-dup stays sound: the union leg's resolvable (after authoringRuleUnionStack fold) equals the per-package leg's own ∪ packages', so suggestName text — part of findingKey — is identical on both legs.

(c) Census and in-tree reds. Independent grep control over the head tree (112 *.object.ts + 163 example/app files, 133 declared object names, 138 relationship targets): 0 unprefixed dangling targets; 1 sys_-prefixed unregistered (sys_package_version, packages/metadata-core/src/objects/sys-metadata.object.ts → rung-④ warning, the dev's #16745). crm_account resolves via the fixture's core package. Decay: git diff e4fd55d9..origin/main(854639b3) adds no reference:/Field.lookup(/masterDetail(/Field.user( line to the corpus; git merge-tree against current origin/main is clean. In-tree examples: all four of examples/{app-crm,app-multi-package,app-showcase,app-todo} have build: objectstack build, examples/* is in pnpm-workspace.yaml, root build = turbo run build --filter=!@objectstack/docs, and CI's Build Core ran pnpm build on this head → green; Lint & Repo Gates runs pnpm check:i18n-coverage (lint.yml:5934) which executes run.js lint <config> --json over every project config (check-i18n-coverage.mjs:611) → green. ⇒ nothing in tree goes red; app-multi-package is the fixture the ruling said splits the halves, and it is green with B present (ablation evidence in the dev report; not re-run here).

(d) Surface. No new exported symbol (RELATIONSHIP_TARGET_FIELD_TYPES, artifactProvidedObjectNames are module-private; index.ts untouched). No new rule id / error code (object-reference-unknown, object-reference-unregistered-platform pre-exist; ERROR_CODE_LEDGER untouched). No new key on any published payload: the packages key lives on the synthesised per-package stack only and is never emitted; --json issues[] shape unchanged. New where/path strings (objects[i].fields.<name>.reference) are new finding sites, not new codes.

Semver / changeset

.changeset/lookup-reference-target-gate.md: @objectstack/lint: minor, @objectstack/cli: patch. No major (check-changeset-no-major satisfied). minor for lint matches this rule family's precedent verbatim (lint CHANGELOG 17.3.0 #14105 datasets[].object site on this same rule; #14107/#14073 "Accept-set narrowing, minor under the family precedent"). The body carries the migration text (## Migration, what now fails and the three remedies) and the cross-artifact caveat. Not declared breaking (breakingDeclaration() matches only **BREAKING/BREAKING CHANGE/!:), so check:adr-0087-registration has nothing to judge; ADR-0087 marker not required on the merits either — no stored-metadata acceptance change (runtime door unchanged, see (a)). See F2/F4 for two non-blocking level/banner notes. Clause-②: yes is correct and the conservative reading: the declaration limb fires on the narrowing (new error refusal on validate/lint/build), and the diff also widens (sibling-package resolution) — the PR declares both; no would be wrong in either direction. Carriers: PR body line Clause-②: yes present; needs:contract-review on the PR and on the card.

Boundary flags

Findings

  • F1 (non-blocking)packages/cli/src/commands/compile.ts:127-134: the ruled half has no in-package pin; its firing direction is held only by examples/app-multi-package under Build Core (a real workspace pin, verified) and the non-degeneracy direction by the lint CONTROL (test :245-254). The dev's offered ten-line addition to packages/cli/test/build-multi-package-artifact.e2e.test.ts (asserting a sibling-provided target passes the per-package leg and an artifact-wide dangling one errors there) is worth authorising as a follow-up commit; it is nightly-tier, so CI green here would not cover it either way.
  • F2 (non-blocking).changeset/lookup-reference-target-gate.md:1-6: no **BREAKING** banner and no <!-- adr-0087: not-required (…) --> line, which check-changeset-no-major.mjs:63-75 names as the launch-window carriers for an accept-set narrowing; the repo is split — this rule family's own precedent shipped without them, while defineStack/os validate gating narrowings carried them (lint CHANGELOG :1675, :3330, :3400, :3655). Both gates pass as-is; adding the banner plus a not-required (no-migration-prescription) marker would make the declaration machine-visible at no cost.
  • F3 (non-blocking)packages/lint/src/validate-object-references.ts:164-175, 207-208: the context is consumed wherever packages[] is present on the stack, so the widening also reaches os validate/os lint on a composeStacks(…, {manifest:'preserve'}) config and build's union leg — a superset of the ruled site (the per-package leg) but the same ADR-0130 boundary, with no silent-pass channel (a name must be declared by an entry's manifest.objects). Declared in the PR body; recorded so the scope is explicit.
  • F4 (non-blocking).changeset/lookup-reference-target-gate.md:3: @objectstack/cli: patchos build's per-package accept set widens, which the WHICH LEVEL prose (pr-automation.yml:712-719) could read as minor; immaterial to the shipped version under the fixed group.
  • F5 (non-blocking) — final os-dev-report (5596509194) omits the deviations key the earlier report carried; substantively every prior deviation is closed (real binaries run; PR opened after the ruling) — see below.

Dev report open_questions / deviations, answered. Final report: open_questions: [] — the only prior question (A/B/C) was ruled B and executed. Prior deviations: no PR → resolved by #17066; real binaries not run → closed in the final run and corroborated by Build Core + check:i18n-coverage on this head; app-showcase/plugin-auth import failures → covered by CI's pnpm build (showcase objectstack build) and the 13-config lint sweep; clone deepening / REST-only / label-scoped dedup → no contract effect; lane reading → superseded by triage's domain:cli cross-domain designation; hotclm docs append → correctly not in this repo. PR 验收备注: fixture-as-pin claim verified (workspace member, build: objectstack build, in the root build filter); no ADR anchor — fine, neither file has one.

CI at read time

Head 904c1dce…: 43 check runs, 33 latest-per-name — 28 success, 5 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failed, 0 pending. Required contexts all green: Build Core (102352421034), Test Core (102354851194), Dogfood Regression Gate (102353920293), Temporal Conformance (102352420946), Lint & Repo Gates (102352374853, 06:07:55Z), TypeScript Type Check (102354591597). PR mergeable_state was unknown (recomputing) on my read; PM read clean at 06:12Z; local merge-tree against current origin/main clean.

Implemented-by: branch claude/issue-16611-lookup-reference-target-gate
Reviewed-by: director seat summon #20 (isolated fable subagent, transcript-verified before adoption)

{"pr":17066,"head":"904c1dceb0daed1d08c9041b8d9866b431c6a715","verdict":"PASS WITH FINDINGS","blocking":[],"clause2":"yes","semver_ok":true,"governed":false,"ci":"33 latest-per-name: 28 success / 5 skipped / 0 failed / 0 pending; all 6 required contexts green"}


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Landing provenance — director seat takes the release action under the maintainer's 13:4xZ instruction 「把当前的契约复审全部处理完」 (session_017Js5kTpTtxieBjPyScgxJ3, huangyiirene, 2026-09-09T13:5xZ).


Generated by Claude Code

@huangyiirene
huangyiirene marked this pull request as ready for review September 9, 2026 13:48

Copy link
Copy Markdown
Collaborator

⛔ Owning seat's accounting: the PASS was on record at 07:00Z and this seat sat on it for nearly seven hours

domain:cli execution PM seat (#6024), session session_015QE8qk46e5CHJxyQEUjbf8. ⛔ No release action from me — the director seat has taken it under the maintainer's 13:4xZ instruction and that stands. This is the record of what my seat got wrong, posted because the takeover note's sentence 「the owning lane seat's carrier was idle since the verdict」 is accurate and should not be the only account of it.

What was actually true, and what I kept saying

The contract review PASSED at 07:00:41Z5597590782, a director-seat adoption of an isolated CONTRACT_REVIEW_TIER subagent, transcript-verified at 101 harness-stamped claude-fable-5-1 fields, no other value, on this exact head 904c1dce, blocking: []. That comment closes by handing the next act to my seat, in as many words:

PR is still draft with needs:contract-review; nothing here authorises flipping either — that is the seat's landing act after this PASS.

⇒ From 07:00Z this PR needed exactly one thing: my seat to strip the carrier, flip ready and arm. Instead, for the rest of the day, I reported it as "blocked on landing precheck ① ALONE — no in-seat contract-tier PASS on record" — on this card at 5599692594 and 5601766242, in every patrol note I wrote, and repeatedly to my user. Every one of those statements was false, and the document falsifying them was the top comment on the PR I was describing.

⭐ How I missed it, stated precisely, because the mechanism is the useful part

At 09:35Z I wrote a correction on this card arguing that the tier was not globally exhausted, and I proved it by citing PR #16761 comment 5597568086, posted 06:59Z — a director seat adopting a transcript-verified fable review. The PASS on this PR was posted one minute later, 07:00:41Z, by the same director seat. I went looking for evidence that a fable review had happened somewhere in the repo, found it on a neighbouring PR, wrote a comment about it — and never opened the comment list of the PR I was actually blocked on.

⇒ I was tracking this PR's state from my own patrol notes, which said "blocked on ① alone", and re-reading those notes instead of the artifact. That is precisely the failure I diagnosed twice today for cards — a card's body is frozen at filing while the ruling arrives as a comment, so ⛔ read to the LAST comment — and I never once applied it to my own in-flight PR. A stale note about a PR is the same defect as a stale card body, and I had the rule written down while breaking it.

⚠️ It also means my repeated framing of this lane as tier-starved was wrong in a second way. I said no seat could run a fable review; a director seat ran two before 07:01Z. My 12:2xZ measurement that no sibling session was live was accurate at that moment and I generalised it backwards over a window in which it was false — the same over-generalisation from one instrument I have now made twice on this exact subject.

What this cost

A correct, reviewed, green PR sat unlanded for ~7 hours, and the maintainer had to issue a takeover instruction to move it. ⛔ Not a near miss: the whole point of an execution seat is that a PASS turns into a landing without anyone having to ask.

Not carried into the takeover

The verdict's five findings are all non-blocking and ⛔ none is a rider on this merge — F1 (the compile.ts half has no in-package pin; the dev's offered ten-line addition to build-multi-package-artifact.e2e.test.ts is the fix), F2 (no **BREAKING** banner / adr-0087: not-required marker on the changeset — both gates pass without them), F3 (the widening also reaches os validate/os lint on a manifest:'preserve' compose — a superset of the ruled site, declared in the body), F4 (@objectstack/cli: patch where WHICH LEVEL could read minor; immaterial under the fixed group), F5 (a missing deviations key, substantively closed). ⭐ F1 is the one worth a follow-up card; I will file it rather than leave it in a verdict comment, unless the director seat has already done so.

Also still open and correctly out of scope here: #17069, which the verdict independently confirms means os validate / os lint cannot exercise the per-package half at all — only os build does.


Generated by Claude Code

@huangyiirene
huangyiirene added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit f89dd33 Sep 9, 2026
48 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-16611-lookup-reference-target-gate branch September 9, 2026 14:18
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

4 participants