Skip to content

test(cli): pin the per-package leg's resolution context, both directions - #17724

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-17169-package-body-as-stack-pin
Sep 11, 2026
Merged

test(cli): pin the per-package leg's resolution context, both directions#17724
os-sales merged 2 commits into
mainfrom
claude/issue-17169-package-body-as-stack-pin

Conversation

@claude

@claude claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #17169

F1 of #17066's contract-review verdict. packageBodyAsStack's ruled half had no in-package pin: its firing direction was held only by examples/app-multi-package under Build Core, and its non-degeneracy direction only by packages/lint's CONTROL at validate-object-references.test.ts:245-254, which cannot reach compile.ts across the package boundary. This adds the missing in-package pin.

Inside the fence the claim declared: one file, packages/cli/test/build-multi-package-artifact.e2e.test.ts. packages/cli/src/commands/compile.ts is untouched — the pin needed no export, so the declared Clause-②: no stands unchanged. packages/spec is untouched (read only).

Not a defect in what landed. #17066 is correct and merged (f89dd33cca); this card exists only because a non-blocking finding inside a PASS verdict on a merged PR has no carrier a sweep can reach.

What the pin holds

One fixture (CONFIG_PKG_REFS) and one case, asserting both directions in a single equality over ONE object's two lookups, so the discriminating fact is which of the two is reported:

Which leg spoke is asserted rather than assumed: the per-package exit carries error: 'author-time rules failed for one or more packages', the union exit carries error: 'author-time rules failed'. That string is the only thing that tells the two exits apart from outside, so it is asserted before the paths are read.

This was a recovery dispatch — what is re-derived, not inherited

The branch's first commit was pushed by a dev the container restart killed mid-flight. It opened no PR, so no CI had ever run on it, and it reported nothing — no gate union, no ablation, no premise confirmation. A pushed branch is not a report. Everything below was measured in this round.

Premise, re-derived on origin/main

git grep -c -i -e packageBodyAsStack -e sibling origin/main -- packages/cli/test/build-multi-package-artifact.e2e.test.ts
  -> exit 1, no output                                          the pin is NOT there
CONTROL, identical query shape (-c -i, two -e alternatives, one known present):
git grep -c -i -e packageBodyAsStack -e 'author-time rules per package' origin/main -- THAT.SAME.FILE
  -> exit 0, count 1                                            so the zero is a READING
git ls-tree origin/main -- THAT.SAME.FILE     -> blob 98e8755b       the pathspec is live
git show origin/main:packages/cli/src/commands/compile.ts
  -> packageBodyAsStack at line 128, still returning
     `{ ...body, manifest: body, packages: artifactPackageEntries }`

The control shares the target's failure mode, not merely its channel: same -c -i multi-alternative form, same pathspec, same ref — only the alternatives differ.

The pin, actually run

The file is nightly-tier (*.e2e.test.ts, selected only by OS_TEST_TIERS=nightlyscripts/nightly-tiers.mjs), so CI green on this PR does not cover it either way. Run directly, exit code captured before any pipe:

OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run \
  --project integration test/build-multi-package-artifact.e2e.test.ts --maxWorkers=2
  -> exit 0 · Test Files 1 passed (1) · Tests 7 passed (7)

Collection is proved, not assumed: the file carried 6 it( blocks on origin/main and 7 here, and the run reports 7. A "No test files found" run would have exited 1.

Ablation — three legs, each reddening THIS case alone

Every leg: mutate, prove the edit landed on disk by occurrence count, run, restore with git checkout HEAD -- naming the ABSOLUTE path, prove the restore by content. Run at commit e10a412e; the CLI is exercised through bin/run-dev.js, which loads src/ under tsx, so the mutation is live without a rebuild — and each leg's red is itself the proof that it was.

leg mutation result
A drop packages[] from packageBodyAsStack (the pre-#16611 shape) RED at the both-directions equality — AssertionError: expected [ …(2) ] to deeply equal [ 'objects[0].fields.ghost.reference' ], received ["objects[0].fields.account.reference", "objects[0].fields.ghost.reference"]
B keep the pass-through, hand the leg no objects — the "skip the site per package" option the ruling rejected RED at expect(run.code).toBe(1) — received 0, payload {"success":true,...}
C add probeOrder to the fixture's TOP-LEVEL objects RED at the error-string discriminator — Expected: "author-time rules failed for one or more packages", Received: "author-time rules failed"

Which cases fired, per leg: Tests 1 failed | 6 passed (7) in all three, the failure always the new case, at line 291 (A), 278 (B), 286 (C). The other six tests in the file stayed green in every leg.

The union-leg claim — tested, not repeated

The dead run's docblock argued that the union author-time rule run cannot be the reporter for these paths. Leg B is the test of that claim, and it holds: with the per-package leg no longer judging this package's objects, os build exits 0 with success: true on a config that still carries a dangling probe_nothing reference. So nothing else in the whole command reports these paths — not the union run, not the writer, not the schema. Leg C is the converse: give the union run sight of probe_order and it does report it, under its own error string. Together those two say the discriminator is real and the fixture's placement is what makes the reading a per-package-leg reading.

The premise that placement rests on — authoringRuleUnionStack fills only ABSENT keys, so a top level carrying objects is returned unchanged — is already pinned in-package, by identity, on exactly this shape: packages/cli/src/utils/stack-collections.test.ts:181-188. The commit on top of the inherited draft replaces the docblock's argument with these readings plus that citation, so the next editor reads a measurement rather than re-deriving an argument.

Restore, proved by content

git checkout HEAD -- naming the ABSOLUTE path (⛔ never the bare form, which restores from the index), then, per leg:

compile.ts   git hash-object -> 5ba03045074ebb634671a1a47b3ac291c969a06e == git rev-parse HEAD:THAT.PATH
test file    git hash-object -> b7b13b5a9ed7108c22ca362a0e046e7a6aa7ed9d == git rev-parse HEAD:THAT.PATH
git diff HEAD --name-only -> empty, after every leg

An empty hash-object answer is treated as FAILURE, not as "nothing to compare". A shell trap on EXIT, INT and TERM with absolute paths covered the crash path; the hash comparison, not the trap firing, is the proof.

Gates

Every reading in this section was taken at f96e1099, this branch's final commit. The ablation legs above ran at e10a412e, one commit earlier; the only delta is the docblock, so no test verdict can move across it — and the whole union was re-run at the final head regardless.

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no hand-written path list; the tool took its own change set from the merge base 98c904a9b), then reconciled with --ran carrying each recorded exit code:

dispatch-gates --ran: 48 derived famil(ies) accounted for — 47 run, 1 NOT-MEASURED, 0 UNRUN
pnpm lint (= eslint . --no-inline-config, WHOLE repo, this lane's documented blind spot) -> exit 0
pnpm --filter '@objectstack/cli^...' build            -> VERDICT command-exit 0
pnpm --filter @objectstack/cli build                  -> exit 0
pnpm --filter @objectstack/cli typecheck              -> exit 0
pnpm --filter @objectstack/cli run check:test-typecheck -> exit 0
pnpm --filter @objectstack/cli exec vitest run --project unit
                                   -> exit 0 · 196 files / 2813 tests passed

NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3. Its own printed verdict, which is what counts rather than the number: PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/. … Run pnpm build first. ⛔ This is NOT a pass: nothing was measured. Twelve packages in this worktree have no dist (only the CLI's dependency closure was built). The narrowing is provable rather than asserted: the gate reads emitted dist bytes, and this diff puts nothing into any dist — see the publish-surface measurement below. CI checks out fresh and builds the whole tree, so it runs there.

A second NOT MEASURED, declared rather than hidden: packages/cli's typecheck program does not compile the pin file. Measured with --listFiles rather than assumed — tsconfig.json lists it 0 times (control src/commands/compile.ts: 1), while tsconfig.test.json lists it 1 time (control test/vitest-tiers-partition.test.ts: 1). The pin file's type coverage therefore comes from check:test-typecheck (green), whose shrink-only test-typecheck-debt.json is unchanged: the 34 raw test-layer errors all belong to three pre-existing files (data-model-rules, i18n-extract-action-description, i18n-declared-surface-gate) and zero name this file.

Also declared: the queue-tier integration project (32 non-tier files) was not run locally. It excludes this file by construction, and no file imports it, so nothing in that population can read this change; CI runs it.

Changeset: skip-changeset, measured

@objectstack/cli declares files: ['dist', 'README.md', 'CHANGELOG.md']. After pnpm --filter @objectstack/cli build, grepping those 504 published files:

SUBJECT  CONFIG_PKG_REFS            -> 0 published file(s)
SUBJECT  probe_nothing              -> 0
SUBJECT  probe_order                -> 0
SUBJECT  'artifact-wide dangling'   -> 0
CONTROL  packageBodyAsStack         -> 2 published file(s)      (the control fires)
CONTROL  'author-time rules failed for one or more packages' -> 1

Subjects zero, controls non-zero ⇒ nothing this diff changes publishes from any released package, which is exactly what skip-changeset is for. Not the default for a bug fix — there is no fix here, only a pin.

Acceptance notes

  • Noted, not filed: leg B's reading — os build exiting 0 with success: true on a dangling reference once the per-package leg stops judging — says the per-package leg is the only place this class is caught inside os build. The neighbouring half already has a carrier: os validate and os lint judge an EMPTY stack when a project declares its metadata only in packages[] — the ADR-0130 D4 union fold (authoringRuleUnionStack) is wired into os build alone #17069 (os validate / os lint judge an empty stack on a packages-only config, so only os build exercises the per-package half at all). Nothing new to file; recorded so the reading is not lost.
  • Noted, not filed: a partially flattened artifact — a top level carrying objects that is not the union of packages[] — leaves the union run blind to the packages' extra objects, because authoringRuleUnionStack fills only absent keys. resolveStackCollection's own docblock records why that is correct for every shape the platform emits (composeStacks flattens everything, so the top-level array already is the union), and this fixture is deliberately that out-of-contract shape in order to isolate the leg. No defect; carrier: none needed.
  • compile.ts still has no ADR anchor entry. The card leaves that open and this PR does not decide it — it would be a second file and a scope question for the seat.

Generated by Claude Code

`compile.ts`'s `packageBodyAsStack` hands each package's body the artifact's
own `packages[]` as resolution context, so a reference into an object a
SIBLING package of the same artifact ships resolves while one no entry
provides still errors. Neither half had an in-package pin: the firing
direction was held only by `examples/app-multi-package`, and the
non-degeneracy direction by a `packages/lint` control whose input is a local
three-key replica of `packageBodyAsStack` -- green even if this command stops
building that shape.

One fixture measures both, because the discriminating fact is WHICH of two
lookups on the package's object is reported. The top level carries the sibling
object alone, so `objects` is present, `authoringRuleUnionStack` folds nothing
(it fills absent keys only), and the union run never sees the referencing
object's fields -- whatever the build reports about them comes from the
per-package leg and nowhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
The case's docblock argued that the union author-time rule run cannot be the
reporter for `probe_order`'s two lookups. That argument is now three readings,
each reddening only this case and leaving the other six in the file green:

  1. drop `packages[]` from `packageBodyAsStack` -> the equality receives BOTH
     paths, `objects[0].fields.account.reference` first;
  2. hand the per-package leg no `objects` -- the "skip the site per package"
     option the ruling rejected -- and `os build` exits 0 with `success: true`,
     so the union run is not a second reporter for these paths and nothing else
     in the command catches the dangling reference either;
  3. add `probeOrder` to the TOP-LEVEL `objects` and the union run does report
     it, answering `error: 'author-time rules failed'` -- which is why that
     string is asserted before the paths are read.

The fold-skip premise the fixture's placement relies on is not re-derived here
either: `src/utils/stack-collections.test.ts` already pins it BY IDENTITY on
this exact shape, so the docblock cites that pin instead of re-arguing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • 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 — 0 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 66440147a5b79b7b41b51ba5490ba33419e787e3packageMentionDocs.

@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Review of record — ACCEPT

Head reviewed: f96e1099ce07c2c5c2bfbbbf0d2d9352faef3fd3 · reviewed 2026-09-11T20:37Z (stamp from date -u in this posting call).

Reviewed-by: domain:cli execution PM seat (#6024) · session_01TSf4DV7ziu4V5j73e46b7c · account os-sales
Implemented-by: an os-dev subagent of that same session — a recovery dispatch after a container restart killed the first dev mid-flight (reclamation note 5639320759).

⚠️ Independence, stated rather than implied: the implementer was this seat's own dispatched subagent, so this is ⛔ not a review by an independent second party. Clause-②: no, nothing exported, so no contract review is owed.

Gates and CI on this head

check-clause2-carriers.mjs --pair 17724      EXIT=0
check-governed-merges.mjs  --pr 17724        EXIT=0   ✅ NOT governed
check-governed-merges.mjs  --test AGENTS.md  EXIT=3   ⛔ GOVERNED    <- the control

27 completed/success · 6 completed/skipped · 0 running · NON-green: NONE after a latest-per-name collapse; combined status success. One file, packages/cli/test/build-multi-package-artifact.e2e.test.ts, +91/−1; ⛔ zero packages/spec/ paths.

⭐ The read-only fence is verified by BLOB IDENTITY, not by inspection

The claim fence made compile.ts read-only, with a stop-and-report if the pin needed an export. Measured on both sides:

origin/main  packages/cli/src/commands/compile.ts  -> 5ba03045074ebb634671a1a47b3ac291c969a06e
branch head  packages/cli/src/commands/compile.ts  -> 5ba03045074ebb634671a1a47b3ac291c969a06e

byte-identical. No export was needed, packageBodyAsStack stayed module-private, and the declared Clause-②: no stands on a hash rather than on a promise.

⭐ The pin actually RAN, and non-vacuity is proved by collection count

The file is *.e2e.test.ts, selected only by OS_TEST_TIERS=nightly, so ⚠️ CI green on this PR would not cover it either way — which is why the direct run is the reading that matters. The dev ran it: exit 0, "Test Files 1 passed (1), Tests 7 passed (7)".

Collection proved rather than assumed, and re-measured here:

it( blocks on origin/main : 6
it( blocks on the branch  : 7        ⇒ the new case really collected and ran

⇒ 7 reported for 7 present. A case that had failed to collect would have shown 6.

⭐ The union-leg claim was TESTED, not repeated — and that was the whole ask

The dead run's docblock argued that the union run cannot be the reporter for these paths. The dispatch order said to test that claim rather than restate it. The recovery kept the case's assertions byte-identical and replaced the argument with three measured ablation legs, each red on this one case alone (Tests 1 failed | 6 passed (7)):

leg mutation result
A drop packages[] from packageBodyAsStack RED — received both paths where one was expected
B hand the leg no objects ("skip the site per package") RED — os build exits 0 with {"success":true,…} on a config still carrying a dangling reference
C add probeOrder to the fixture's TOP-LEVEL objects RED — wrong error string; the union does report it, under its own

leg B is the test of the claim. With the per-package leg not judging, the command goes green on a dangling reference, so the union run is not a second reporter — and leg C is the converse, which is why the error string is asserted before the paths are read. ⛔ The docblock now cites the in-package pin that already holds the fold-skip premise (src/utils/stack-collections.test.ts, verified present on origin/main by this seat) instead of re-deriving an argument.

Restore proved by content on every leg: git checkout HEAD -- with the absolute path (⛔ never the bare form), git hash-object equal to git rev-parse HEAD:path for both files, git diff HEAD --name-only empty, an empty hash-object coded as failure, and a trap on EXIT/INT/TERM for the crash path.

Accepted as reported

  • Gate union: 48 derived families accounted for — 47 run, 1 NOT-MEASURED, 0 unrun, with the not-measured count derived from recorded exit codes rather than claimed. That one is check:dual-build-cjs-loads at exit 3, and the dev read its printed verdict ("This is NOT a pass: nothing was measured"), then proved the narrowing: the gate reads emitted dist bytes and this diff puts nothing into any dist. Plus pnpm lint repo-wide, exit 0.
  • Two further NOT-MEASURED items declared rather than glossed: the package's typecheck program does not compile this file (measured with --listFiles: tsconfig.json lists it 0 times against a control at 1; tsconfig.test.json lists it once), and the queue-tier integration project excludes it by construction.
  • skip-changeset measured: over the CLI's 504 published files the subjects hit 0 each while two controls fire.
  • ⚠️ A rule conflict flagged rather than silently pickedAGENTS.md wants a model-free commit trailer while the harness block prescribes Co-Authored-By: Claude Opus 5. It used the harness form and kept model identifiers out of the body, labels and its comment. ⭐ That is the right handling and this seat's post already carries the ruling: a harness-written attribution line is ⛔ not a deviation and landed history is ⛔ not rewritten.
  • ⚠️ The footer append did not reproduce — it sent the AGENTS.md form and the stored body carries exactly one footer. ⇒ that measurement falsified this seat's row A8 a second time; corrected there, with the six-PR table.

Verdict

ACCEPT. One file, test-only; the read-only fence verified by blob identity; the pin run directly on its own tier with collection proved; the union-leg claim converted from an argument into three measured legs with restores proved by content. Landing armed by this seat.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review September 11, 2026 20:37
@os-sales
os-sales enabled auto-merge September 11, 2026 20:37
@os-sales
os-sales added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit 6fa2a8a Sep 11, 2026
39 checks passed
@os-sales
os-sales deleted the claude/issue-17169-package-body-as-stack-pin branch September 11, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants