fix(spec): resolve permission-set and seed object references against the release artifact, not the single stack - #18212
Conversation
…eStacks Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W Co-authored-by: Claude <noreply@anthropic.com>
…artifact Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 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 35ff00c5ba22da41c002493ad3e4cd7a13178975 && git checkout 35ff00c5ba22da41c002493ad3e4cd7a13178975
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 75237a9a8950de387530e1fdcd8695ab3e92807e 0424329a899a2be3d21cccb720883560350fd51f && git checkout -B drift-repro 75237a9a8950de387530e1fdcd8695ab3e92807e && git merge --no-ff 0424329a899a2be3d21cccb720883560350fd51f
node scripts/docs-audit/affected-docs.mjs --json 75237a9a8950de387530e1fdcd8695ab3e92807e
|
…ed classes Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W Co-authored-by: Claude <noreply@anthropic.com>
|
pm-note (⛔ not a contract-review record — no Closing the promise made in the ACCEPT verdict: CI is greenThe verdict on #18202 said the ruling was about the work, not about the lights, and that this seat would watch to green. It is green.
Pre-landing check ② — measured here so the reviewer does not re-derive it
PM ruling, already recorded on #18204 and on the finding card:
⛔ So #14122 gets no review label, and this row is not a reason to hold the PR. Mechanism card: #18214 — which now has four independent measurements behind it: both devs hit it on their own PRs, and this seat reproduced it on #18212 and #18213 with the exit code captured before any pipe. What this seat is NOT doing⛔ No carrier stripped, no ready flip, no queue action, no merge. Clause ② puts the review of a contract increment at the Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgmentsBasis: the diff read from the REST endpoint (4 files, +579/−48 against merge-base
② Semver level
The changeset body does not stand as written. It ships verbatim into ③ Boundary flagsFlag 1 — should view Flag 2 — an opt-in on a never-composed stack cannot be falsified. Acceptable, and the analogy to Flag 3 — Remediation that turns this into a PASS, all inside the PR's own surface: (a) in Implemented-by: VERDICT: FAIL Generated by Claude Code |
…t narrows The artifact pass added for #18202 ran the two artifact-scoped collectors over every composed input, including inputs the strict parse never saw. Three corrections, all inside this PR's own surface: - `collectSeedDataObjectErrors` and `collectPermissionGrantObjectErrors` now guard their own shapes the way `composeStacks`'s step-3 concat pass does. A non-array `permissions` / `data` is announced through `warnMalformedCollectionKey` (deduplicated per key, so the two passes speak once) and skipped; a non-object entry, and a non-string `object`, carry no reference for the rule to resolve and are skipped. Before this, an unparsed input that `origin/main` composes with a warning raised a bare `TypeError` with no `code` and no `status` — a crash outside the ADR-0112 envelope, in the file whose refusal discipline is that envelope. - The compatibility claim is replaced by the invariant that actually holds. An input that passed the strict `defineStack` parse cannot newly fail at composition; an input that BYPASSED it (`strict: false`, a hand-built stack object) is checked for these two rules at composition for the first time and a dangling reference in it is now refused. That narrowing is declared in the docstring, the changeset and the fixtures instead of being claimed away. - `composeStacks` returns `stacks[0]` untouched, so the artifact pass never runs for a single input. The option docstring and the docs bullet now say "in a composition of two or more packages" rather than stating the guarantee unqualified. Fixtures pin all three: the refusals on unparsed inputs, the malformed-shape guards, and the one-input boundary with its two-package contrast. Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W Co-authored-by: Claude <noreply@anthropic.com>
…not iterable The first ablation leg measured the previous fixture as GREEN under the mutation: a string value iterates its own characters, so removing the `Array.isArray` guard changed nothing for it. Map format — the shape a hand-built stack most plausibly carries for a collection key — is not iterable, so the fixture now fails without the guard and the ablation reads red for all six malformed cases. Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsBasis: the diff read from the REST endpoint (4 files, +857/−48 against merge-base
② Semver level
The body, sentence by sentence. FROM paragraph — accurate (S2's message is the quoted one). TO paragraph and example — accurate (O1/O2). "Nothing else widens … Precision, recommended rather than required: "For an input that passed the strict ③ Boundary flagsFlag 1 — the third REST write (PR body PATCH) against a two-write budget. Correct disposition. The same dispatch (card comment Flag 2 — Flag 3 — deliberately not merged forward. Judged in ① item 9: acceptable for a targeted rework, on the evidence that the reviewed Remediation that turns this into a PASS — one clause, two surfaces, nothing else moves: in Implemented-by: VERDICT: FAIL Generated by Claude Code |
… a warning
The changeset said an input the artifact pass newly refuses "previously
composed with only a warning". Measured on main, that class of input produces
no diagnostic at all: `composeStacks` calls `validateCrossReferences` zero
times and `console.warn` zero times, and its one `warnMalformedCollectionKey`
site fires on `declared.length !== arrays.length` — a collection key that is
not an array. The newly-refused inputs carry well-formed arrays with a
dangling reference, so that condition never holds.
The clause conflated two populations and handed an upgrading reader a false
self-test ("we never saw a warning, so this is not us") in text that ships
verbatim into CHANGELOG.md. It now reads "where it previously composed with no
diagnostic at all — the existing non-array warning covers a malformed
collection key, not a dangling reference".
Same stroke, the precision the review asked for: the no-op half of the
invariant holds for an input that passed the strict parse AND did not opt in.
An opted-in input also passed that parse but resolved against its own objects
plus the names it listed, and checking a listed name against the real artifact
is what this pass is for — so it can fail here by design. The qualifier is
added in the changeset, in the `collectArtifactCrossReferenceErrors` docstring
and in the fixture file's header, which carried the same sentence.
Text only: no schema, no rule, no fixture and no docs page changes, and the
changeset level stays `minor`.
Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsBasis: the diff read from the REST endpoint (4 files, +865/−48 against merge-base
② Semver level
The changeset body at head, sentence by sentence: headline (a sibling-owned object can be granted on and seeded into) — O1. FROM: both classes had to name a same-stack object, and the quoted message — S2 on ③ Boundary flagsFlag 1 — the third "did not opt in" carrier (test-file header). Judged in ① item 3: comment-only, JS-identical, declared, and the header would otherwise contradict the block it cites. Acceptable. Flag 2 — the removed phrase quoted once in the PR body. Judged in ① item 4: a citation under an explicit REMOVED banner whose "nowhere else" claim is verified; zero in the changeset. Not a residual, not blocking. Flag 3 — Flag 4 — M11, an array-form Flag 5 — #18239 ( Flag 6 — Flag 7 — 13 commits behind Implemented-by: VERDICT: PASS Generated by Claude Code |
|
pm-note — 本席位的落地次序错了,已重挂闸门;第三轮 PASS 记录仍有效,但不再覆盖将要落地的树 发生了什么
19 秒。 我清完标就把 head 移走,等于自己作废了自己的清标。 机读判据当场抓到,原话:
⇒ 正确次序,记下来免得再犯⛔ 错的:复核 → 清标 → merge-forward → 落地 复核自己在 Flag 7 里就写明了 merge-forward 是「PM 落地前仍欠的一步」。我把它排在了复核之后,而不是之前。 这次 merge 本身是干净的,已核
下一步
第三轮的 PASS 记录 Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgmentsBasis: the head fetched from
② Semver level
The body, which ships verbatim into ③ Boundary flagsFlag 1 — the merge-forward and its disposition. The merge itself is clean and honest (① item 1). The ordering error is real and is the dispatch seat's own: the gate was cleared at 03:38:14Z (PR) / 03:38:16Z (card) and the head moved at 03:38:33Z, so for 19 seconds a cleared gate stood on a tree no record had judged. The correction is adequate: the seat did not proceed to ready/enqueue, re-hung both carriers at 03:40:20Z / 03:40:22Z, recorded the error on the PR ( Flag 2 — Flag 3 — is the gate correctly re-hung on both carriers? Read from the label events, not the note: PR #18212 Flag 4 — Flag 5 — #18239 ( Flag 6 — the PR body's stale process lines (① item 10). Recommended: one PATCH replacing "this branch is 12 commits behind Flag 7 — docs-drift on this head. The bot's run on the merge Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #18202
Part of #14122 — the epic tracking the one-artifact/N-packages family. That tracker stays open.
Rework after a contract-review FAIL
The isolated contract review on head
b0072379returned FAIL and the bolded compatibilityclaim this body used to carry was false. Three corrections landed on this branch; nothing else
moved, and the changeset level stays
minor(the review judged the level correct and the bodywrong).
Array.isArrayguards and 0 malformed warnings, while step 3 of the samecomposeStacksbody carries 3 and 1 — so a malformedpermissions/dataon an unparsed input raised a bareTypeError(nocode, nostatus, outside the ADR-0112 envelope) wheremaincomposes with a warningwarnMalformedCollectionKeyand skipped, and a non-object entry — or a non-stringobject— carries no reference for the rule to resolve and is skippedfor (const stack of stacks)over every input, whiledefineStackreturns atif (!strict)before cross-reference validation ever runs — sostrict: falseand hand-built inputs are newly refusedif (stacks.length === 1) return stacks[0];is unchanged, so a one-input composition never reaches the pass, and the docs bullet and option docstring overstated the guaranteeRound 2 of the rework — one clause, two surfaces
The second contract review re-measured all three findings above and recorded them resolved
(36 probes; the guards hold with no
TypeError, thestrict: falserefusals are pinned by fivepositive fixtures, the one-input qualifier is fenced, and it confirmed the false-green fixture
correction). It failed on one sentence. The changeset and this body described the newly-refused input's prior
fate with the phrase
previously composed with only a warning— quoted here as the text that wasREMOVED, and appearing nowhere else in this PR.
That was false. On
mainthis class of input produces no diagnostic at all —composeStackscalls
validateCrossReferenceszero times andconsole.warnzero times, and its onewarnMalformedCollectionKeysite fires ondeclared.length !== arrays.length, i.e. a collectionkey that is not an array. The newly-refused inputs carry well-formed arrays holding a dangling
reference, so that condition never holds. The clause conflated two populations and handed an
upgrading reader a false self-test — "we never saw a warning, so this is not us" — in text that
ships verbatim into
CHANGELOG.md.Corrected in the changeset and here. Taken in the same stroke, the precision the review recommended:
the no-op half of the invariant holds for an input that passed the strict parse and did not opt
in, which is now said in the changeset, in the
collectArtifactCrossReferenceErrorsdocstring andin the fixture file's header, which carried the same sentence. Text only — no schema, no rule, no
fixture and no docs page moved, and the level stays
minor.What changed
defineStackvalidated cross-references per stack, so "defined inobjects" and "defined in the artifact" were the same question. In an ADR-0130 artifact they are two questions, and the two accepted records could not both hold once thetype: apppackage owns objects of its own:type: apppackage, because a set is authored per ROLE and ADR-0086 D3 gives it exactly one owning package.So the app package's sets necessarily grant on its modules' objects, and every such grant was refused. Measured downstream on
objectstack-ai/hotcrmclaude/issue-1907-sales-app-service-module(be11c07) as 18 grants across 7 sets;data[].objectwas refused the same way, which is why that branch had to push its seed rows into the module.This PR gives the two reference classes that legitimately cross a resolution scope of their own, and moves their refusal to the pass that owns that scope.
DefineStackOptions.artifactObjectspermissions[].objectsanddata[].objectresolve against the stack's own objects plus these names. Nothing else in the function moves.The measurement that chose the mechanism
The dispatch proposed gating the widening on the stack declaring non-empty
manifest.dependencies. Measured against the reporter's own repro, that gate does nothing for this defect, and the finding is worth recording because the card's title carries the same framing:src/salesapp.objectstack.hotcrmtype: app, nodependenciescrm_case— this is the failing stacksrc/serviceapp.objectstack.hotcrm.servicetype: module,dependencies: { 'app.objectstack.hotcrm': '^3.1.0' }The dependency edge runs module to app, which is the only direction ADR-0116's topological order admits: the module registers after the package it extends. The app therefore cannot name its modules as dependencies without inverting that order. Resolving against the referencing package's declared dependency closure would refuse exactly the shape the addendum makes mandatory.
⇒ The resolution scope is the artifact, which is what ADR-0130 D1 says it is — not the dependency closure, and not "anything, checked later".
The two classes that cross, and the ones that must not
Every object-name reference class the check covers, with the per-class ruling:
permissions[].objectsdata[].object(seed)hooks[].objectnavigationobjectNamerequiresObjectremains the declared per-item escape)data.object,mappings[].targetObject,actions[].objectName(both positions)The first two blocks of the new fixture assert that
artifactObjectsdoes not widen the hook and navigation refusals even when the name is listed.The refusal MOVED —
composeStacksis the named passcomposeStacksdid not re-run any cross-reference check at all (it only checked composed action-key collisions), so deferring without adding one would have opened a hole. It now re-checks the two artifact-scoped rules over the composed object set:code—STACK_CROSS_REFERENCE_INVALID— and the samestatus: 422;composeStacks artifact cross-reference validation failed (N issues):— because only the pass differs.A name
artifactObjectsclaims and no package in the artifact defines is therefore still refused, loudly, at a named pass — in a composition of two or more packages. That fixture is an acceptance criterion of the card, and it isArtifactPassin the new test file.What it can and cannot newly refuse
An earlier revision of this body claimed the pass "cannot newly refuse a composition that passes today". That is false, and the contract review measured it twice. The invariant that actually holds is two statements, not one:
defineStackparse AND did not opt in cannot newly fail here. It does not read the option (recording the option on the returned stack would put a composition-time concern onObjectStackDefinitionSchema, i.e. on every artifact already built). It re-runs the two rules for every input, which is a no-op for such an input — its references already resolved against its own objects, and those are a subset of the composed set. The qualifier is load-bearing: an input that did opt in also passed the strict parse, but it resolved against its own objects plus the names it listed, and checking a listed name against the real artifact is precisely what this pass is for — so an opted-in input can fail here by design. That is theArtifactPassfixture, not a gap.defineStack(config, { strict: false })returns beforevalidateCrossReferencesruns at all, and a hand-built stack object never enters it. Such an input carrying a danglingpermissions[].objectskey ordata[].objectis now refused at composition where it previously composed with no diagnostic at all — the existing non-array warning covers a malformed collection key, not a dangling reference.The second is a real narrowing of what
composeStacksaccepts. It is the Prime Directive #12 direction — reject off-spec input loudly — and it is now declared: the changeset states it,collectArtifactCrossReferenceErrors's docstring states it, and the fixture pins it rather than leaving it to be rediscovered as a regression.What the narrowing is not is a licence to crash. Both collectors guard their own shapes, so an unparsed input carrying a non-array
permissions/data, anullentry, or a non-stringobjectis warned about and skipped the way step 3's concat pass already treats it — never a bareTypeErroroutside the ADR-0112 envelope.Inputs declaring no objects are still skipped, inheriting
validateCrossReferences's ownobjectNames.size === 0leniency verbatim; that leniency is why hotcrm#1449's measurement never saw the defect, and it is pinned as untouched in both directions.Known boundary — this pass runs only for two or more packages.
composeStacksreturnsstacks[0]untouched for a single input, so a one-package composition never reaches the pass, and neither does a stack that is never composed. That is the population whoseartifactObjectsclaim stands unverified — the same shape asstrict: false, for the same reason. Every reader-facing statement of the guarantee now carries the qualifier.os buildcomposes.Compatibility — #14122 §6
Nothing in this PR touches the load path, the artifact schema or any authored key.
defineStackis unchanged for a stack that does not passartifactObjects: the widened scope is literally the sameSetobject as the narrow one when the option is absent. The three existing cross-reference pin suites pass unchanged, andcheck:authorable-surface/check:api-surfaceare both green without regeneration.composeStacksis where the acceptance set does move, for exactly one population — inputs that bypassed the strict parse — and that is stated above, in the changeset and in the docstring rather than claimed away. §6's own subject (a stored single-manifestartifact replayed through the load path) is untouched by it: the artifact pass runs at composition time, on the inputs, and never at load.Verification
Re-run in a worktree off
origin/maind4554d4, at0c21631c— the rework head. Every exit codebelow was redirected to a file and captured before any pipe.
pnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typecheckcheck:test-typecheck)vitest run src/stack-artifact-crossref.test.tspnpm --filter @objectstack/spec check:generatednode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 106; each run singly with its exit code captured before any pipe, then--ranreconciledpnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2pnpm lint(eslint . --no-inline-config) — the whole union, not a narrowing0c21631cSeven of the 106 first returned a PREREQUISITE reading — six
exit 3(PREREQUISITE NOT MET) andcheck:skill-examplesexit 1whose body says "packages/client-react/dist holds no .d.tsdeclarations — build first, then re-run". Those are NOT-MEASURED, not reds: all seven were re-run
after the closure build and all seven exit 0. They are
check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt,check:docs-transcript-drift,@objectstack/lint check:doc-formula-expressions,@objectstack/lint check:doc-security-postureand
@objectstack/spec check:skill-examples.Carried from the pre-rework head
b0072379, and why it is still valid: the consumer sweep(
turbo run typecheck --filter='...@objectstack/spec', the DOWNSTREAM direction, 139/139) was notre-run. The rework changes two internal function bodies, docstrings, a fixture, a changeset and a
docs page — no exported type, no published
exportsentry.check:api-surfaceis green with noregeneration owed and
check:generatedreports all 15 artifacts up to date, so the publisheddeclaration surface is byte-unchanged and no import site can have moved.
Known staleness, declared: this branch is 12 commits behind
origin/mainanddispatch-gates.mjssays so in its own words — six of the files its derivation reads changed acrossthat range. The 106 is therefore this tree's derivation, not
origin/main's. The branch isdeliberately not merged forward: it is a targeted rework of an open PR that a reviewer is about to
re-read against its recorded FAIL, and CI runs the full farm on the merge result anyway.
Reverse verification (one-off, not committed tests)
Two legs, each committed first, then mutated, then restored under a
trapwhose restore path isabsolute and spelled
git checkout HEAD --. Both restores are proven by blob identity and an emptygit diff HEAD, never by an exit code. No build step is involved: these tests import./stack.zodby relative path, not through the package
exports.Leg 1 — the shape guards. Prediction, written before the run: removing the two guards turns the
malformed-input block red with bare
TypeErrors. Mutation proven on disk —Array.isArrayinsidethe two collectors 2 to 0,
warnMalformedCollectionKeycall sites 3 to 1, blob8debc710vs HEAD690c80dd. Observed: 5 failed / 25 passed, every failure a bareTypeError(
permissions is not iterable,datasets is not iterable,Cannot read properties of null,name.startsWith is not a function). The sixth case in that block stays green under the mutationand is honestly a control, not a guard pin:
objects: nullon a permission entry was alreadycovered by the pre-existing
!grants || typeof grants !== 'object'line.One correction worth recording, because the first attempt of this leg measured a false green: the
non-array
permissionsfixture originally used a string, and a string is iterable, so removingthe guard changed nothing for it. It now uses map format — not iterable — and fails without the
guard. The fixture was fixed and the leg re-run from the committed state.
Leg 2 — the one-input boundary. Prediction: deleting
if (stacks.length === 1) return stacks[0];turns the two "accepts a one-input composition" fixtures red and leaves the rest green, proving those
fixtures actually observe the early return rather than merely asserting an accept. Mutation proven on
disk (occurrences 1 to 0, blob
e7c7b384vs HEAD690c80dd). Observed: exactly 2 failed / 28passed, both in the one-input block. Restored,
git status --porcelainempty.docs-drift-checkadvisory — decided, not deferredThe advisory flagged five pages. Decisions, so a reviewer does not re-derive them:
content/docs/getting-started/glossary.mdx— namescomposeStacks()once, in passing ("the authoring form … or composes one withcomposeStacks()"). Nothing it states is falsified. No edit.content/docs/getting-started/examples.mdx— its "rules worth knowing before you split a product this way" list is the page a reader lands on for exactly this shape, and nothing already there becomes untrue. But the list would have been silently missing the rule this PR creates, so it gains one bullet naming the two artifact-scoped classes, theartifactObjectsspelling, and the hook exception. That is the documentation half of the option, not scope creep.content/docs/releases/v17/*(three pages) — release-owned under AGENTS.md's Documentation Guardrails, and read-only in a code PR. Not edited here, and none of them was found to be wrong: they describe shipped behaviour this PR does not change.Note the advisory was computed on the merge commit
318def6e, not on this PR's head, so a local re-derivation can legitimately differ.Acceptance notes
check-plugin-teardown-shape.mjs --self-testexits 1 on a shallow checkout — it pins its positive control to a commit the clone cannot reach and says so rather than passing. Fetching that one commit clears it (47 cases pass). Not a finding, and not this PR's to change.PREREQUISITE NOT MET) until the package closure is built:check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt,@objectstack/lint check:doc-formula-expressions. All four are green after the closure build. Working as designed; recorded so the exit-3 readings in this PR's history are not read as reds.pnpm check:role-wordcaught it in the same local run and it now says position. Recorded because the word is ratcheted repo-wide and the gate is easy to meet only if you know it exists.StackCrossReferenceErrordocstring said "there is exactly ONE raise site". With the artifact pass there are two, so the paragraph now names both and says why they share onecode: the code names the rule family (what a machine matches), the header names the pass (what a reader needs).composeStacksstep 2 raises a bareTypeErrorfor a non-arrayobjectson an unparsed input, and always has.mergeObjectsiteratesstack.objectswith no guard; probed against the builtdist,objectsas a map andobjectsas a number both giveTypeError: stack.objects is not iterable,codeundefined,statusundefined.mergeObjectsis not touched by this diff (git diff d4554d4f5..HEAD -- packages/spec/src/stack.zod.tshas no hit on it), so this is a pre-existingmaindefect of the same family as the one this rework fixes, not a regression. It is reported for filing rather than fixed here: the rework dispatch is explicitly scoped to the three review findings, and widening it would be the second time this PR's scope moved. Dedupe words:composeStacks,mergeObjects, non-arrayobjects, bareTypeError, ADR-0112 envelope.isPlatformObjectNamecallsname.startsWithwith no string guard. No live call site can reach it with a non-string now that the seed-data collector filters ontypeof === 'string'(the other caller passesObject.keysoutput), so this is noted, not filed.warnMalformedCollectionKeycall added inside each collector can never be the FIRST to warn for its key in today's code: step 3 ofcomposeStacksruns before step 3b and warns for the same key, and the reporter deduplicates per key for the module's lifetime. It is kept anyway so the rule cannot become the silent one if a future caller reaches it first, and the fixture asserts the observable consequence — exactly ONE warning per key, not two.Follow-ups this PR does not take
data.object,mappings[].targetObjectandactions[].objectNamestay stack-scoped. Whether a co-owning package may carry a view over, an import mapping into, or an action bound to a sibling's object is a contract question no record answers, and the repro does not need it. Flagged for thedomain:specseat rather than decided here.Errorenvelope gap on these refusals is finding(spec):defineStack's cross-reference refusals are bareErrors — no ADR-0112code/status— so five REFUSED item classes in the ADR-0130 matrix are distinguishable only by message text #14552's, deliberately untouched — and the class this PR moves keeps its envelope shape: both passes raiseStackCrossReferenceError,codeSTACK_CROSS_REFERENCE_INVALID,status422,issuesone entry per finding.defineStack's per-stack cross-reference validation refusespermissions[].objectsanddata[].objectthat name an object owned by ANOTHER PACKAGE OF THE SAME ARTIFACT — so ADR-0130's co-owned artifact and the "permission sets stay whole in the app package" addendum cannot both hold once the app package owns objects #18202 rather than only in a review thread.artifactObjectsthreaded intocreateHotCrmAppStackand a local@objectstack/speclink; the sister checkout is read-only here and that branch belongs to another card, so this PR relies on the fixture, which reproduces the two-package shape at its smallest.packages/cli,packages/lintandexamples/**are untouched — the sibling dispatch on cli:os build/os devper-package author-time rules refuse an action param's record-pickerreferenceto a dependency's object (object-reference-unknown), while the composed pass accepts it and ADR-0130 R1 accepts the field-level equivalent #18204 owns them this round.Generated by Claude Code