test(cli): pin the union fold by the finding's pedigree, not by its exit code - #19369
Conversation
…xit code `union-fold-command-parity.test.ts` claimed to pin `authoringRuleUnionStack`, the fold that makes the author-time rule table's INPUT non-empty on an option-B (`packages[]`-only) stack. It could not: since #18677 and #18778 all three doors also run `runPerPackageAuthoringRules`, which judges each package body as its own stack and raises the SAME rule at the SAME path on this fixture. Measured by ablation at 13d5294 — the fold deleted: 6/6 green; the per-package pass deleted: 6/6 green; BOTH deleted: 3 red. The two mechanisms are told apart by the finding's pedigree: the per-package pass prefixes `where` with `package '<id>' — ` and the union run renders it bare. Each refusal case now asserts that prefix is ABSENT, and a new positive control on a per-package-only fixture asserts the same suite can make it PRESENT, so the absence is a measurement rather than a regex that never matches. A structural falsifier is not available for this rule class and the header says so with the reason: the per-package pass is a superset of the union run for reference rules, so a dangling name dangles in both views. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf
…ho-satisfied-controls-sweep
📓 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): |
Seat grading — ACCEPT. The sweep found a third impostor mechanism, and proved the remedy shape is load-bearing.What the seat verified independently, ⛔ not taken from the report
⭐ The repair is additive for the right reason, and I checked that rather than assuming it
⭐ And the pedigree assertion is paired. The file says so itself: "a ⛔ And the regex is not this file's guess at a format: it is derived from the pass that owns the prefix, with three sibling files pinning the same shape. The finding
⇒ neither mechanism alone was load-bearing — the definition of the defect, and a third impostor: neither the echo (#18878) nor the id tail (#18962 round 3). ⭐ The leg that makes this round worth more than its diffThe historical impostor reconstruction — falsifier removed and Coverage, and why the other 20 are reported rather than silent21 fixtures were enumerated by the property — a control asserting that a survivor or resolution exists — ⛔ not by the three files the card body tabulates. 30 ablation legs across 9 mechanisms, every mutation and restore proven on disk. 17 went red under their own claimed mechanism. 3 stayed green and are each argued, which is the part a sweep usually omits: ⭐ ⛔ A sweep that lists only what it changed cannot be checked for coverage. This one can. The generalisation — filed as #19371The dev recorded it and declined to file, on the ground that a repo-wide sweep is a scope proposal rather than a reproducible defect. ⭐ That reasoning is right, and the insight was still too load-bearing to leave in a report:
⛔ No commit to Generated by Claude Code |
Fixes #18897
Clause-②: no
The sweep, and what it found
The card's discriminant: a control is satisfied by an impostor whenever some mechanism other than the one under test can supply the asserted value in that fixture. The method is ablation — delete the mechanism a control claims to pin, re-run, and read the colour. A control that stays green was never pinning it.
21
packages/clifixtures were enumerated by the property (a control that asserts a survivor or a resolution exists), not by the three files the card body tabulates, and every one of them was ablated. Baseline first: 21 files / 225 tests, all green at the dispatch base13d52947d8.One control family was impostor-satisfied, and its impostor is a THIRD mechanism — neither the echo nor the id tail.
test/union-fold-command-parity.test.tsclaimed to pinauthoringRuleUnionStack, the fold that makes the author-time rule table's INPUT non-empty on an option-B (packages[]-only) stack. It could not. Since #18677 (os validate) and #18778 (os lint) all three doors ALSO runrunPerPackageAuthoringRules, which judges each package body as its own stack — and on this fixture that pass raises the SAME rule at the SAME path and refuses with the same exit code.union-fold-command-parity.test.tsauthoringRuleUnionStacknever foldsrunPerPackageAuthoringRulesyields no findingsEither mechanism alone kept every case green. Only deleting both turned the refusal cases red. The control was correct when it was written (#17069) and became impostor-satisfied when a sibling pass was added to the same doors a year of cards later — which is the generalisation this PR contributes back to the card.
The repair
The two mechanisms are told apart by the finding's pedigree.
runPerPackageAuthoringRulesprefixes thewhereof every finding it raises withpackage 'ID' —; the union run renderswherebare. Measured on that fixture, with only the fold's presence moving:So each refusal case now asserts that prefix is ABSENT, and a new positive pedigree control asserts the same suite can make it PRESENT — on a fixture whose only finding really is per-package-only (
coredeclaresob_account.industry,ordersowns the view that displays it: the falsifier shape PR #18878 landed). Without that pair the negative assertion would be satisfied by a prefix this suite never produces at all.Verified in both directions, from the committed state:
One half of the prescribed remedy is not available here, and the header says so
The card's remedy shape is a real falsifier AND a pedigree assertion. The falsifier half cannot be built for this rule class, and that was measured rather than assumed: the per-package pass is a SUPERSET of the union run for reference rules (
utils/artifact-packages.ts: "the per-package run is STRICTER"), because each body is judged with the artifact's ownpackages[]handed in as resolution context. A name no package provides therefore dangles in BOTH views, under every arrangement of packages. Two candidate union-only fixtures were built and probed; neither produced a finding the per-package pass could not also raise. The pedigree is the whole discriminant here, and the positive control is what makes it falsifiable.The full ablation table
Every fixture is reported, including the ones that needed no change — a sweep that lists only what it changed cannot be checked for coverage.
test/union-fold-command-parity.test.tsauthoringRuleUnionStackfoldtest/union-fold-command-parity.test.tstest/union-fold-command-parity.test.tstest/validate-per-package-authoring-parity.test.tstest/validate-per-package-authoring-parity.test.tsexpected 0 to be greater than 0test/build-text-face-advisory-count.test.tstest/build-text-face-advisory-count.test.tstest/lint-per-package-authoring-parity.test.tstest/lint-per-package-authoring-parity.test.tstest/lint-per-package-authoring-seam.test.tstest/lint-per-package-authoring-seam.test.tstest/validate-per-package-authoring-seam.test.tstest/per-package-dedup-positional-echo.test.tssrc/utils/collect-docs.package-docs.test.tsdocsPackageRefsid-TAIL branchsrc/utils/collect-docs.package-docs.test.tsdocsPackageRefsFULL-ID branchsrc/utils/artifact-packages.test.tspackageBodyAsStackresolution contexttest/lint-handwritten-checks-package-fold.test.tsauthoringRuleUnionStackfoldsrc/utils/format.metadata-stats-package-fold.test.tsauthoringRuleUnionStackfoldsrc/utils/stack-collections.test.tsauthoringRuleUnionStackfoldsrc/utils/stack-collections.test.tsresolveStackCollectionpackages legtest/info-detail-package-fold.test.tsresolveStackCollectionpackages legsrc/utils/nav-contribution-groups.test.tsartifactPackagesreports no packagessrc/utils/nav-contribution-groups.package-id.test.tsartifactPackagesreports no packagessrc/utils/permission-set-name-collisions.test.tsartifactPackagesreports no packagestest/build-multi-package-artifact.e2e.test.tspackageBodyAsStackresolution contexttest/build-multi-package-artifact.e2e.test.tspackagesfrom the artifacttest/build-package-docs-attachment.e2e.test.tsdocsPackageRefsid-TAIL branchtest/build-package-docs-attachment.e2e.test.tsdocsPackageRefsFULL-ID branchtest/compile-artifact-packages.e2e.test.tsartifactPackagesreports no packagesartifactPackagestest/compile-artifact-packages.e2e.test.tspackagesfrom the artifacttest/validate-build-gate-parity.test.tsauthoringRuleUnionStackfold bodytest/init.test.tspackages:occurrences are the pnpm-workspace key, and no control in it asserts a per-package survivor or resolution. It appears in the card's keyword tally as a false positiveEvery ablation was performed through
scripts/ablation-replace.mjs, so each mutation carries its own on-disk proof (anchor count moved, blob hash changed) and each restore is proven by blob equality against HEAD plus an emptygit diff HEAD. No ablation is left on disk.Fences observed
packages/cli/src/utils/collect-docs.tsis not edited by this PR — [finding] docs/duplicate-name stands on a justification ADR-0048 §3.4 retired — record what the rule now rests on #19248 holds that file. Two ablation legs mutated it transiently inside this worktree only, each restored with a proven blob match; the diff touches one file,packages/cli/test/union-fold-command-parity.test.ts.src/, so a doc in this repo's own ADR-0130 reference fixture (src/packages/PKG/docs/) is dropped silently — exit 0, nothing printed #18965) are out of scope. The key was reverted to its positional form for ONE measurement — the historical-impostor reconstruction described in the report comment — and restored.Verification
At
e2b7d13d0f:pnpm --filter @objectstack/cli exec vitest run --project unit— 220 files / 3114 tests, all passpnpm --filter @objectstack/cli exec vitest run --project integration— 51 files / 430 tests, all pass, run in two halves for the foreground cappnpm --filter @objectstack/cli typecheck— passnode scripts/pm/dispatch-gates.mjs --commandsderived 46 gate families; all 46 run, all exit 0, reconciled with--rancarrying each exit code: "46 derived families accounted for — 46 run, 0 NOT-MEASURED (a DERIVED zero)"pnpm lint(repo-wide,eslint . --no-inline-config) — exit 0, run in full rather than narrowedpnpm --filter '@objectstack/cli^...' buildand the fullturbo run buildover the package farm — both exit 0check:dual-build-cjs-loadsandcheck:type-check-debtfirst answeredPREREQUISITE NOT MET(exit 3, which both scripts state is neither a pass nor a finding) because only the CLI closure had been built. Both were re-run after the prescribed full build and both exit 0.Acceptance notes
Noted, not filed — observations from the sweep that are not reproducible defects, contract violations or authoring traps:
test/build-package-docs-attachment.e2e.test.tscannot distinguishdocsPackageRefs' full-id branch from its id-tail branch. That is deliberate in its fixture and stated in its own comments, so it is a coverage boundary rather than a defect. Next toucher: whoever changesdocsPackageRefs' spelling set — the unit pins next door already cover both branches, and they go red.test/validate-per-package-authoring-seam.test.tsand the non-vacuity case oftest/lint-per-package-authoring-seam.test.tsde-duplicate against an EMPTY union by construction, so their survivors are not asserted to be union-invisible. Both headers say so explicitly; the stronger claim lives in the parity files. Next toucher: none currently in flight.test/init.test.tsas an unexamined multi-package fixture. It is not one; the match is the pnpm-workspacepackages:key.Generated by Claude Code