Skip to content

Commit 4ed1ab9

Browse files
test(cli): pin the union fold by the finding's pedigree, not by its exit code (#19369)
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/cli` fixtures 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 base `13d52947d8`. **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.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 (`os validate`) and #18778 (`os lint`) all three doors ALSO run `runPerPackageAuthoringRules`, 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. | ablation | `union-fold-command-parity.test.ts` | |:--|:--| | `authoringRuleUnionStack` never folds | **6/6 GREEN** | | `runPerPackageAuthoringRules` yields no findings | **6/6 GREEN** | | BOTH of the above | **3 RED** (the three refusal cases) | Either 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**. `runPerPackageAuthoringRules` prefixes the `where` of every finding it raises with `package 'ID' — `; the union run renders `where` bare. Measured on that fixture, with only the fold's presence moving: ```text fold intact • object "ob_order" · field "ghost": … fold ablated • package 'com.example.ob' — object "ob_order" · field "ghost": … ``` 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 (`core` declares `ob_account.industry`, `orders` owns 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: | run | result | |:--|:--| | repaired file at HEAD | 9/9 GREEN | | union fold ablated | **3 RED** — the three refusal cases (was 6/6 green before this change) | | per-package pass ablated | **3 RED** — the three pedigree controls; refusal cases correctly stay green | ### 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 own `packages[]` 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. | fixture | mechanism deleted | red? | verdict | |:--|:--|:--|:--| | `test/union-fold-command-parity.test.ts` | `authoringRuleUnionStack` fold | **NO — 6/6 green** | **IMPOSTOR (third family)** — repaired here | | `test/union-fold-command-parity.test.ts` | per-package pass findings | **NO — 6/6 green** | same control, other mechanism | | `test/union-fold-command-parity.test.ts` | both of the above | YES — 3 | confirms neither alone is load-bearing | | `test/validate-per-package-authoring-parity.test.ts` | per-package pass findings | YES — 1/4 | correct (only the lit control; the parity pins go vacuous, as the card predicts) | | `test/validate-per-package-authoring-parity.test.ts` | the fixture's falsifier view | YES — 1/4 | correct; `expected 0 to be greater than 0` | | `test/build-text-face-advisory-count.test.ts` | per-package pass findings | YES — 3/5 | correct | | `test/build-text-face-advisory-count.test.ts` | the fixture's falsifier view | YES — 1/5 | correct | | `test/lint-per-package-authoring-parity.test.ts` | per-package pass findings | YES — 2/5 | correct (documented already-correct; verified, not inherited) | | `test/lint-per-package-authoring-parity.test.ts` | the fixture's falsifier view | YES — 1/5 | correct | | `test/lint-per-package-authoring-seam.test.ts` | per-package pass findings | YES — 2/6 | correct | | `test/lint-per-package-authoring-seam.test.ts` | the fixture's falsifier view | YES — 1/6 | correct | | `test/validate-per-package-authoring-seam.test.ts` | per-package pass findings | YES — 2/6 | correct for its narrower claim (it de-duplicates against an EMPTY union by construction, and its header says so) | | `test/per-package-dedup-positional-echo.test.ts` | per-package pass findings | YES — 5/6 | correct by construction | | `src/utils/collect-docs.package-docs.test.ts` | `docsPackageRefs` id-TAIL branch | YES — 12/32 | correct | | `src/utils/collect-docs.package-docs.test.ts` | `docsPackageRefs` FULL-ID branch | YES — 2/32 | correct — and the 30 staying green is the #18962 reading reproduced | | `src/utils/artifact-packages.test.ts` | `packageBodyAsStack` resolution context | YES — 2/4 | correct; it already carries its own contextless CONTROL leg | | `test/lint-handwritten-checks-package-fold.test.ts` | `authoringRuleUnionStack` fold | YES — 5/7 | correct | | `src/utils/format.metadata-stats-package-fold.test.ts` | `authoringRuleUnionStack` fold | YES — 3/8 | correct (the top-level-only and empty-stack rows rightly stay green) | | `src/utils/stack-collections.test.ts` | `authoringRuleUnionStack` fold | YES — 3/16 | correct | | `src/utils/stack-collections.test.ts` | `resolveStackCollection` packages leg | YES — 2/16 | correct | | `test/info-detail-package-fold.test.ts` | `resolveStackCollection` packages leg | YES — 7/7 | correct | | `src/utils/nav-contribution-groups.test.ts` | `artifactPackages` reports no packages | YES — 6/9 | correct | | `src/utils/nav-contribution-groups.package-id.test.ts` | `artifactPackages` reports no packages | YES — 4/4 | correct | | `src/utils/permission-set-name-collisions.test.ts` | `artifactPackages` reports no packages | YES — 5/10 | correct | | `test/build-multi-package-artifact.e2e.test.ts` | `packageBodyAsStack` resolution context | YES — 1/7 | correct | | `test/build-multi-package-artifact.e2e.test.ts` | compile drops `packages` from the artifact | YES — 2/7 | correct | | `test/build-package-docs-attachment.e2e.test.ts` | `docsPackageRefs` id-TAIL branch | YES — 2/3 | correct | | `test/build-package-docs-attachment.e2e.test.ts` | `docsPackageRefs` FULL-ID branch | **NO — 3/3 green** | correct, NOT an impostor: its fixture directory is an id tail by construction and its header says so. It never claims the full-id spelling. Recorded as a coverage boundary, not a defect | | `test/compile-artifact-packages.e2e.test.ts` | `artifactPackages` reports no packages | **NO — 4/4 green** | correct, NOT an impostor: it reads the written artifact, which is not produced through `artifactPackages` | | `test/compile-artifact-packages.e2e.test.ts` | compile drops `packages` from the artifact | YES — 1/4 | correct; this IS its mechanism | | `test/validate-build-gate-parity.test.ts` | `authoringRuleUnionStack` fold body | **NO — green** | correct, NOT an impostor: it is a source-text WIRING pin with its own positive control (the export must exist) and fabricated negative controls. Ablating the body leaves the wiring, which is what it asserts | | `test/init.test.ts` | not ablated | n/a | EXCLUDED, with evidence: its `packages:` 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 positive | Every 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 empty `git diff HEAD`. No ablation is left on disk. ## Fences observed - `packages/cli/src/utils/collect-docs.ts` is **not** edited by this PR — #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`. - The de-duplication key (#18893) and the docs scan depth (#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 pass - `pnpm --filter @objectstack/cli exec vitest run --project integration` — 51 files / 430 tests, all pass, run in two halves for the foreground cap - `pnpm --filter @objectstack/cli typecheck` — pass - `node scripts/pm/dispatch-gates.mjs --commands` derived 46 gate families; all 46 run, all exit 0, reconciled with `--ran` carrying 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 narrowed - `pnpm --filter '@objectstack/cli^...' build` and the full `turbo run build` over the package farm — both exit 0 `check:dual-build-cjs-loads` and `check:type-check-debt` first answered `PREREQUISITE 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.ts` cannot distinguish `docsPackageRefs`' 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 changes `docsPackageRefs`' spelling set — the unit pins next door already cover both branches, and they go red. - `test/validate-per-package-authoring-seam.test.ts` and the non-vacuity case of `test/lint-per-package-authoring-seam.test.ts` de-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. - The card's keyword tally lists `test/init.test.ts` as an unexamined multi-package fixture. It is not one; the match is the pnpm-workspace `packages:` key. --- _Generated by [Claude Code](https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent a88a973 commit 4ed1ab9

1 file changed

Lines changed: 172 additions & 0 deletions

File tree

‎packages/cli/test/union-fold-command-parity.test.ts‎

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,55 @@
4949
* that fires on any `packages[]` at all — would satisfy the failing case alone.
5050
* The pair asserts what the card actually claims: that the option-B stack is
5151
* READ, not that it is rejected.
52+
*
53+
* ## ⚠️ [#18897] The refusal cases were satisfied by an IMPOSTOR — and the
54+
* ## impostor is a SIBLING PASS that arrived after this file was written
55+
*
56+
* These three refusal cases claimed to pin `authoringRuleUnionStack` — the fold
57+
* that makes the rule table's INPUT non-empty on an option-B stack. They could
58+
* not. Since #18677 (`os validate`) and #18778 (`os lint`) all three doors ALSO
59+
* run `runPerPackageAuthoringRules`, which judges each `packages[]` entry as its
60+
* own stack. That pass sees this fixture's one package body, raises the SAME
61+
* rule at the SAME path, and refuses with the same exit code. ⇒ `exit 1` +
62+
* `contains(RULE)` + `contains(RULE_PATH)` is satisfied by EITHER mechanism, so
63+
* the control could not tell the fold from its sibling.
64+
*
65+
* ⛔ Not reasoned about — measured by ablation, at `origin/main` 13d52947d8:
66+
*
67+
* ablation this file
68+
* `authoringRuleUnionStack` never folds 6/6 GREEN
69+
* `runPerPackageAuthoringRules` yields no findings 6/6 GREEN
70+
* BOTH of the above 3 RED
71+
*
72+
* ⭐ Either mechanism alone kept every case green; only deleting both turned the
73+
* three refusal cases red. That is what a control satisfied by something other
74+
* than the mechanism it names looks like from the inside — green, and green for
75+
* the wrong reason.
76+
*
77+
* ## What discriminates them: a PEDIGREE, ⛔ not a count and ⛔ not an exit code
78+
*
79+
* `runPerPackageAuthoringRules` prefixes the `where` of every finding IT raises
80+
* with `package '<id>' — `; it owns that prefix, and three sibling files pin the
81+
* same regex. The union run renders `where` bare. Measured on THIS fixture, same
82+
* command, with only the fold's presence moving:
83+
*
84+
* fold intact • object "ob_order" · field "ghost": …
85+
* fold ablated • package 'com.example.ob' — object "ob_order" · field "ghost": …
86+
*
87+
* So each refusal case now asserts the refusal carries NO per-package prefix —
88+
* the finding is the UNION run's, which is the mechanism this file is about.
89+
*
90+
* ⚠️ A real falsifier — a fixture in which the impostor structurally CANNOT
91+
* raise this finding — does not exist for this rule class, and that was measured
92+
* rather than assumed: the per-package pass is a SUPERSET of the union run for
93+
* reference rules (`utils/artifact-packages.ts`: "the per-package run is
94+
* STRICTER"), because each body is judged with the artifact's own `packages[]`
95+
* handed in as resolution context. A name no package provides therefore dangles
96+
* in BOTH views, under every arrangement of packages. ⇒ here the pedigree is the
97+
* whole discriminant, and the negative assertion is made load-bearing by the
98+
* POSITIVE control at the foot of this file — a fixture whose finding really is
99+
* per-package-only. Without that pair, "the prefix is absent" would be satisfied
100+
* by a prefix this suite never produces at all.
52101
*/
53102

54103
import { describe, expect, it } from 'vitest';
@@ -69,6 +118,15 @@ const AUTHORING_COMMANDS = ['validate', 'lint', 'build'] as const;
69118
const RULE = 'object-reference-unknown';
70119
const RULE_PATH = 'objects[0].fields.ghost.reference';
71120

121+
/**
122+
* [#18897] The `where` prefix `runPerPackageAuthoringRules` puts on every
123+
* finding IT raises — the one thing that tells the union run's finding apart
124+
* from the per-package pass's copy of it. ⛔ Not this file's guess at a format:
125+
* the pass owns the prefix, and `validate-/lint-per-package-authoring-parity`
126+
* and `build-text-face-advisory-count` pin the identical regex.
127+
*/
128+
const PER_PACKAGE_WHERE = /package '[^']+' — /;
129+
72130
/**
73131
* The card's repro verbatim: no top-level `objects`, one `packages[]` entry
74132
* carrying an object whose `ghost` lookup points at an object that does not
@@ -100,6 +158,73 @@ const optionBStack = (reference: string): Record<string, unknown> => ({
100158
],
101159
});
102160

161+
/**
162+
* [#18897] The POSITIVE half of the pedigree pair: a project whose ONLY
163+
* author-time finding is one the union run genuinely cannot see.
164+
*
165+
* `core` owns `ob_account`; `orders` owns the view that displays
166+
* `ob_account.industry`. Folded into one union the field HAS a consumer and
167+
* nothing is raised; judged per package, `core` declares a field nothing in
168+
* `core` reads — so the survivor is the per-package pass's alone and carries its
169+
* `where` prefix. It is the falsifier shape PR #18878 landed in
170+
* `lint-per-package-authoring-parity.test.ts`, and it is here for one job: to
171+
* prove that {@link PER_PACKAGE_WHERE} is a prefix this suite CAN observe, so
172+
* the refusal cases' `toBe(false)` is a measurement and not the silence of a
173+
* regex that never matches anything. ⛔ Do not remove the view to "simplify" it.
174+
*/
175+
const perPackageOnlyStack = (): Record<string, unknown> => {
176+
const coreManifest = {
177+
id: 'com.example.obflip.core', name: 'obflip core', namespace: 'ob',
178+
version: '1.0.0', type: 'app', engines: { protocol: '^17' },
179+
};
180+
const coreObjects = [{
181+
name: 'ob_account', label: 'Account', pluralLabel: 'Accounts', sharingModel: 'private',
182+
fields: {
183+
name: { name: 'name', type: 'text', label: 'Account Name', required: true },
184+
industry: { name: 'industry', type: 'text', label: 'Industry' },
185+
},
186+
}];
187+
const coreApps = [{
188+
name: 'ob_crm', label: 'OB CRM',
189+
navigation: [{
190+
id: 'sales_group', type: 'group', label: 'Sales',
191+
children: [{ id: 'nav_accounts', type: 'object', objectName: 'ob_account', label: 'Accounts' }],
192+
}],
193+
}];
194+
const ordersManifest = {
195+
id: 'com.example.obflip.orders', name: 'obflip orders', namespace: 'ob',
196+
version: '1.0.0', type: 'module', engines: { protocol: '^17' },
197+
dependencies: { 'com.example.obflip.core': '^1.0.0' },
198+
};
199+
const ordersObjects = [{
200+
name: 'ob_order', label: 'Order', pluralLabel: 'Orders', sharingModel: 'private',
201+
fields: {
202+
name: { name: 'name', type: 'text', label: 'Order Number', required: true },
203+
account: { name: 'account', type: 'lookup', label: 'Account', reference: 'ob_account' },
204+
},
205+
}];
206+
const ordersViews = [
207+
{
208+
name: 'ob_account_list', label: 'Account List', object: 'ob_account',
209+
list: { label: 'Account List', columns: ['name', 'industry'] },
210+
},
211+
{
212+
name: 'ob_order_list', label: 'Order List', object: 'ob_order',
213+
list: { label: 'Order List', columns: ['name', 'account'] },
214+
},
215+
];
216+
return {
217+
manifest: coreManifest,
218+
objects: [...ordersObjects, ...coreObjects],
219+
apps: [...coreApps],
220+
views: [...ordersViews],
221+
packages: [
222+
{ manifest: { ...ordersManifest, objects: ordersObjects, views: ordersViews } },
223+
{ manifest: { ...coreManifest, objects: coreObjects, apps: coreApps } },
224+
],
225+
};
226+
};
227+
103228
interface Run {
104229
code: number;
105230
output: string;
@@ -152,6 +277,23 @@ describe('#17069 — os validate and os lint judge the option-B stack, not an em
152277
`os ${command} named ${RULE} at a different path than the other doors — the three commands ` +
153278
`must report one finding one way`,
154279
).toContain(RULE_PATH);
280+
// ⭐ [#18897] PEDIGREE. Everything above this line is satisfied by the
281+
// per-package authoring pass, which raises the same rule at the same path
282+
// on this one-package body and refuses with the same code — measured: with
283+
// `authoringRuleUnionStack` ablated all six cases here stayed GREEN, and
284+
// only ablating the per-package pass TOO turned these three red. The union
285+
// run renders `where` bare; the per-package pass prefixes it. So the
286+
// absence of that prefix is what makes this case a reading of the FOLD.
287+
expect(
288+
PER_PACKAGE_WHERE.test(run.output),
289+
`os ${command} refused this stack through the PER-PACKAGE authoring pass, not through the ` +
290+
`union fold this file pins: the finding carries ${String(PER_PACKAGE_WHERE)}, the prefix ` +
291+
`runPerPackageAuthoringRules puts on its own findings. The fold is what makes the rule ` +
292+
`table's INPUT non-empty on an option-B stack, and with it gone this command is refusing ` +
293+
`for a reason #17069 did not buy. ⛔ Do not answer this by deleting the assertion — the ` +
294+
`pedigree is the only thing here that can tell the two mechanisms apart.` +
295+
`\n--- output ---\n${run.output}`,
296+
).toBe(false);
155297
},
156298
180_000,
157299
);
@@ -171,4 +313,34 @@ describe('#17069 — os validate and os lint judge the option-B stack, not an em
171313
},
172314
180_000,
173315
);
316+
317+
it.each(AUTHORING_COMMANDS)(
318+
'⭐ PEDIGREE CONTROL — os %s DOES render the per-package prefix when the finding is the pass\'s own',
319+
(command) => {
320+
// The other half of the pair above. A `toBe(false)` on a regex is only a
321+
// measurement if the same suite can make it true; without this case an
322+
// absent prefix and a prefix nothing ever emits read identically — which
323+
// is the whole class of defect #18897 is about, one level up.
324+
const run = runCommand(command, perPackageOnlyStack());
325+
expect(
326+
run.code,
327+
`os ${command} exited ${run.code} on the per-package-only fixture. Its single finding is an ` +
328+
`ADVISORY (field-no-consumers), so every door reports it and none of them fails on it.` +
329+
`\n--- output ---\n${run.output}`,
330+
).toBe(0);
331+
expect(
332+
PER_PACKAGE_WHERE.test(run.output),
333+
`os ${command} raised no per-package-prefixed finding on a fixture whose only finding the ` +
334+
`union run cannot see (core declares ob_account.industry; orders owns the view that ` +
335+
`displays it). Either the pass stopped running on this door, or the prefix was re-spelled ` +
336+
`— and in both cases the refusal cases above are asserting the absence of something this ` +
337+
`suite no longer produces.\n--- output ---\n${run.output}`,
338+
).toBe(true);
339+
// …and it is that field, not some other advisory that happens to be
340+
// prefixed: the pedigree names the member, never just the shape.
341+
expect(run.output, `os ${command} prefixed a finding, but not the one this fixture is built on`)
342+
.toContain('industry');
343+
},
344+
180_000,
345+
);
174346
});

0 commit comments

Comments
 (0)