Repository navigation
fix(cli): os validate and os lint judge the ADR-0130 D4 union-folded stack - #17524
Conversation
…stack
A project whose definitions live only in `packages[]` — the ADR-0130 D4
artifact shape — was judged by both commands as if it declared nothing:
the input they handed the author-time rule table was an empty stack, so
every rule reported nothing and both exited 0. `os build` folds the
packages back in via `authoringRuleUnionStack` before running the same
table and refuses the same stack.
Both call sites now hand the rule table the stack that helper returns —
the one fold `compile.ts` already calls, not a second one. A stack that
still carries its collections comes back by identity, so single-package
projects are unaffected by construction. Rule INPUT only: neither
command's output, `--json` payload nor `scoreMetadata` sees the fold.
Measured through the real binaries on the card's repro:
before os validate 0 · os lint 0 (no finding) · os build 1
after os validate 1 · os lint 1 · os build 1, all three
object-reference-unknown at objects[0].fields.ghost.reference
Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
…tack Behavioural half — `test/union-fold-command-parity.test.ts` drives the card's repro through the three real binaries and asserts all three exit 1 naming `object-reference-unknown` at one path, plus the clean control that keeps the case from passing on a command that simply refuses every `packages[]` project. Source-level half — one more `it()` in the existing gate-parity file, over the same AUTHORING_COMMANDS list the #12297 lesson put there: every door must hand both rule tiers a `authoringRuleUnionStack(...)` stack. Carries a positive control on the helper so a rename fails loudly rather than turning the guard vacuous. Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt Co-authored-by: Claude <noreply@anthropic.com>
…ion-fold-validate-lint
Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt Co-authored-by: Claude <noreply@anthropic.com>
…ion-fold-validate-lint
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 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 b146ebd15818c1b9a96bf46bfa7cbf729227d9e2 && git checkout b146ebd15818c1b9a96bf46bfa7cbf729227d9e2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f8e5790593ed6da5aecb600477a704a3c1db7951 7191b900013a592ea5d1ba0dbe1d8f3c19175a12 && git checkout -B drift-repro f8e5790593ed6da5aecb600477a704a3c1db7951 && git merge --no-ff 7191b900013a592ea5d1ba0dbe1d8f3c19175a12
node scripts/docs-audit/affected-docs.mjs --json f8e5790593ed6da5aecb600477a704a3c1db7951
|
The two findings the acceptance notes promised, now filedBoth were found driving this card's controls, both are out of scope here, and neither is repaired in this PR.
Neither is addressed here; #17527 and #17528 both remain open. Both carry their own repro, their located cause, and the same observation about ⭐ To be explicit about what triage asked for: no in-repo example goes red. All four exit 0 on both commands, byte-identical to before, and the acceptance notes carry the per-example identity measurement that shows why. Generated by Claude Code |
Review — ACCEPT (
|
Fixes #17069
What was wrong
A project whose definitions live only in
packages[]— the ADR-0130 D4 / option-B artifact shape, no collections at the top level — was judged byos validateandos lintas if it declared nothing. Both handed the author-time rule table an empty stack, so all 44 rules reported nothing and both exited 0.compile.tsfolds the packages back in first, throughauthoringRuleUnionStack, and refuses the same stack.Two of the three authoring gates were certifying an unread project as clean, silently, at exit 0 — and in the worst direction of the #4409 weakest-gate class, because
os validateis the fast inner-loop check an author runs before shipping.What changed
validate.tsandlint.tsnow hand the rule table the stackauthoringRuleUnionStackreturns — the one foldcompile.tsalready calls, imported, not reimplemented. Two files, one import and two wrapped members each.It is a rule input only, exactly as it is in
compile.ts: neither command's output,--jsonpayload noros lint'sscoreMetadatapath sees the folded stack, and a stack that still carries its top-level collections comes back by identity, so every single-package project is unaffected by construction.compile.tsandutils/stack-collections.tswere read-only references for this card and are untouched.Acceptance notes
Before / after, through the real binaries
The card's minimal repro (
objectstack.config.ts, no top-levelobjects, onepackages[]entry whoseghostlookup references a non-existentob_nowhere), driven withbin/run-dev.json this branch:os validate✓ Validation passed,Data: 0 Objectsobject-reference-unknownatobjects[0].fields.ghost.referenceos lintos buildobject-reference-unknownNon-vacuity control, same shape with
reference: 'ob_order'(a target that exists): all three exit 0, before and after. The fold makes the option-B stack read, not rejected.What the in-repo examples do now that the gates actually read them — nothing changes, and the reason is measurable
Triage asked for this statement explicitly. All four in-repo example projects exit 0 on both commands after the change, and their verdicts are byte-identical to before it. No example goes red, so there is no second finding of that kind to file.
That is not luck, and it is not "the gates still read nothing": the option-B emitter half of ADR-0130 D4 (#14512) has not landed, so
composeStacks(..., { manifest: 'preserve' })is still additive — every in-repo config still carries its flattened top level andpackages[].authoringRuleUnionStackonly ever fills collections the top level does not carry, so on all four it returns the caller's stack by identity and the rule table's input is unchanged.Measured, per example, over the same
loadConfig+normalizeStackInputthe commands use:The control is what makes the four
trues a reading rather than a constant: on the card's option-B stack the same probe answersfalseand folds one object in.examples/app-multi-packageis the one that carriespackages[]today, and it carries the flattened copy beside it — which is exactly why it is identity.os buildwas reporting the identical finding on the same tree all along.Reverse verification (ablation)
Both command files reverted to their pre-fix content (
git checkout BASE -- the two paths), mutation proven on disk by marker count (authoringRuleUnionStack: validate.ts 3 → 0, lint.ts 4 → 0), then the two guards re-run:The
os buildcase and both clean controls stayed green through the ablation — the pin discriminates the two doors this card is about, rather than reddening on anything. Restored withgit checkout HEAD -- …and proven byte-identical: emptygit diff HEADplusgit hash-objectequal to theHEADblob on both files.Tests
packages/cli/test/union-fold-command-parity.test.ts(new) — the behavioural half, next toauthoring-rule-command-parity.test.ts. Drives all three real binaries over the card's repro and over the clean control.os validate's rule run is an expression inside the oclif command body with no exported seam, so a spawn is the only way to reach it — the same reason the option-B acceptance pin never covered it. Integration tier by behaviour (childProcess,entryBasename), queue tier by name.packages/cli/test/validate-build-gate-parity.test.ts— one moreit()over the sameAUTHORING_COMMANDSlist theos lintnever surfaces ADR-0087 conversion notices — it normalizes with noonConversionNoticesink, the #3782 parity gapos buildwas in #12297 lesson put there, asserting every door hands both rule tiers a folded stack, with a positive control on the helper so a rename fails loudly instead of going vacuous.Verification run
pnpm --filter @objectstack/cli typecheck— green (tsc --noEmit+check:test-typecheck: the test layer compiles undertsconfig.test.json, so the new file is type-checked).vitest --project unit— 194 files / 2685 tests passed.vitest --project integrationon the new file and its sibling — 2 files / 17 tests passed.node scripts/pm/dispatch-gates.mjs --ran …— 62 derived / 62 run / 0 NOT-MEASURED / 0 UNRUN, every family carrying its exit code.check:dual-build-cjs-loadsfirst answeredPREREQUISITE NOT MET(exit 3) on a partialdist/; the six missing packages were built and it then measured green.Noted, not filed here
os validate's metadata summary still reads the top level only: on a clean option-B project it printsData: 0 Objectsand⚠ No objects defined — this stack has no data modelfor a stack that declares one. That iscollectMetadataStats, a different reader from the rule table, and outside this card — the fix here is deliberately scoped to the rule INPUT, as it is incompile.ts. Filed separately rather than folded in; see the report on #17069.Clause-②: no — this narrows what passes back to an already-declared contract (
validating-metadata.mdx: "anything that can fail a build failsos linttoo"). It widens no accept set and adds no public surface.Generated by Claude Code