test(cli): hold field-level and action-param object references in agreement on the per-package leg - #18213
Conversation
…object at BOTH reference levels
The per-package author-time leg hands each package body the artifact's own
`packages[]` as resolution context, so a reference into a sibling package's
object resolves while a name no entry provides still errors. That was measured
at the FIELD level only. This adds the ACTION-PARAM level — the record picker's
`reference` — to both pins, so the two levels are held in agreement:
- `src/utils/artifact-packages.test.ts` (new, queue tier): the real
`artifactPackages` + `packageBodyAsStack` + the real rule, with a CONTROL
leg proving the context is what resolves the name. The lint-side test runs
the same rule against a local replica of this shape and stays green if the
CLI stops producing it; the end-to-end pin carries the `*.e2e` name and
therefore runs only in the nightly population.
- `test/build-multi-package-artifact.e2e.test.ts`: the same pair of names now
also appears as action params on the probe object, so one equality carries
four verdicts through the real command.
No behaviour change: both levels already agree on this tree.
Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
pm-note (⛔ not a contract-review record — no CI is complete and clean on this head
The re-grade this PR asks for, and the one reading that does NOT settle itThe claim declared What DOES support the re-grade is the file list, not the tell gate: the diff is two test files and nothing else ( Pre-landing check ② — measured, so it is not re-derived
Why this seat is not rendering the review itselfThe ACCEPT verdict on #18204 put this PR's clause-② re-grade with the contract seat, and this seat is not going to quietly reverse that the same afternoon by rendering the record itself. Noting the staffing fact plainly, since it is what the wait actually depends on: the ⛔ No carrier stripped, no ready flip, no queue action taken here. Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgments
② Semver levelNone — no changeset, ③ Boundary flagsDeviation 1 — no changeset, Deviation 2 — no production code at all: acceptable, on the strength of ①.3 and of the ablations below. The ruling's two substantive demands — the refusal MOVES rather than disappears, and the two reference levels agree — hold on this tree and are now pinned at both levels. Measured on the head with each mutation proven on disk and restored from
So the pins hold against the regression the card reports (A1) and against its lint-side half (A3), at both levels, in both tiers. One measured limit, and the PR body overstates it: the acceptance note claims "the end-to-end and queue-tier pins above now catch" a Deviation 3 — GitHub writes over REST with the token rather than the MCP write tools the dispatch named: outside this review's subject. Not a contract reading; reported, with reads and writes enumerated, rather than silently resolved. Nothing here to grade. Deviation 4 — Deviation 5 —
Residual exit 4 from CI on this head: 42 check-runs — 31 Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #18204
Part of #14122 — the ADR-0130 tracker stays open; nothing here is a closing keyword for it.
Headline: the reported defect does not reproduce on
main— the missing thing was the pinThe card reports that
os build's per-package author-time leg refuses an action param'srecord-picker
referenceinto a dependency's object while the composed pass accepts it, andwhile ADR-0130 R1 accepts the field-level equivalent. Measured on this tree, both levels agree
and have since #16611: the per-package leg resolves the sibling's object at the field level AND
at the action-param level, and still refuses a name no package of the artifact provides.
What DOES reproduce the card, exactly, is the published
@objectstack/cli@17.4.0that thedownstream repro builds against. Read out of that installed tarball:
and its
@objectstack/lint@17.4.0carries neitherartifactProvidedObjectNamesnor the fieldreferencesite. That is the whole asymmetry the card saw: at that release the action-paramsite is judged against one package's own objects (refused), while the field site is not judged
at all (so it looks accepted). Both halves moved in #16611. The downstream fix is a pin bump,
not a change here.
So this PR changes no behaviour. It adds the pin the card asks for — "the refusal must move,
not disappear, and a fixture must prove it" — at the level the card is about.
Measurements
All on
examples/app-multi-package(a real two-package artifact: moduleordersdepends onapp
core, which ownscrm_account), through the realobjectstack build:reference: 'crm_account'(app package's object)crm_contract(no package provides it)object-reference-unknownatobjects[0].actions[0].params[0].referenceThe example edits were a throwaway probe and are not in this diff (restored;
git diff HEADempty).
What the two new pins hold
packages/cli/src/utils/artifact-packages.test.ts(new, queue tier) — the realartifactPackages+packageBodyAsStack+ the real rule. One equality carries fourverdicts:
crm_accountabsent at BOTH levels (resolved throughpackages[]), the twodangling names present at BOTH levels (the refusal moved, it did not disappear). A CONTROL
leg with no context reports all four, so the pin can fail.
packages/cli/test/build-multi-package-artifact.e2e.test.ts— the existing end-to-end casefor the same seam gains the action-param level, so the same four verdicts are also read
through the real command.
Why both: the lint-side rule test runs against a local REPLICA of
packageBodyAsStack's outputand stays green if the CLI stops producing that shape; the end-to-end file carries the
.e2ename, so
OS_TEST_TIERSputs it in the NIGHTLY population and the merge queue never runs it.The new file is the queue-tier half.
Ablation — the pins fail when the wiring is removed
Mutating
packageBodyAsStackback to the 17.4.0 shape ({ ...body, manifest: body }), on-diskmutation proved by text counts (removed-text 1 to 0, injected-text 1) and by blob hash
(
febc6ad4to9b4019eb):both failing with the same diff — the two
crm_account/probe_accountpaths appearing,including
objects[0].actions[0].params[0].reference, which IS the card's symptom reproducedon this tree. Restored from
HEAD:git diff HEADempty, blob back tofebc6ad4.Tests and gates
pnpm --filter @objectstack/cli exec vitest run --project unit— 207 files, 2962 tests, allpass (the new file included).
OS_TEST_TIERS=nightly ... --project integration build-multi-package-artifact— 7 pass. Runbecause the diff touches an integration-layer file.
pnpm --filter @objectstack/cli typecheck— exit 0.pnpm lint(eslint . --no-inline-config, repo-wide, not narrowed) — exit 0 over 6754 filesat
1a4624be8.node scripts/pm/dispatch-gates.mjs --commandsderived 57 families; 55 run green, 2 recordedexit 3 (PREREQUISITE NOT MET —
check:dual-build-cjs-loadsandcheck:i18n-coveragebothneed a full-tree build; this worktree built only the CLI closure). Reconciled with
--ran:57 derived, 57 accounted, 0 UNRUN. One further self-test,
check-plugin-teardown-shape --self-test, exits 1 on this container because the clone is shallow and it cannot reach itspinned fixture commit — its tree-judging leg passes.
Changeset
skip-changeset, measured rather than assumed:packages/clipublishesfiles: ["dist", "README.md", "CHANGELOG.md"], the diff is two test files, and neither name appears anywhereunder
packages/cli/distafter a build, while the positive control (packageBodyAsStack)does. Nothing published moves. The dispatch asked for a
patchentry describing what theper-package pass now accepts; that sentence would be false — it has accepted it since #16611,
whose own changeset carried it.
Clause 2
The claim on the card declares
Clause-2: yeson the expectation that the fix widens theacceptance set. The measurement falsifies that premise for this PR: the accept-set is
byte-identical before and after.
needs:contract-reviewis attached anyway, per the claim, sothe contract seat can re-grade rather than have the label quietly dropped by the executor.
Acceptance notes
Observed while measuring, not filed and not fixed here:
compile.tsholds two adjacent variables for the same noun —artifactPackageEntries(theraw
packages[], which is the resolution context the rule reads) andpackageEntries(the{ index, id, body }view).packageBodyAsStack's second parameter is typedunknown, sopassing the wrong one type-checks and silently restores exactly this refusal. The end-to-end
and queue-tier pins above now catch it; the type does not. Carrier: whoever next refactors
that block.
objectExtensions[].fields[].referenceis a third level with a third verdict — not judged atall, deliberately and documented in
validate-object-references.ts, whose header names thecross-artifact case as its own card. Reported per the dispatch's ruling 2 rather than widened
into.
os validateruns the union pass only, so it has no per-package leg to disagree with;os lint's per-package leg passes the same rawpackages[]this one does;os devspawnscompile. No fourth verdict found.Generated by Claude Code