Skip to content

Commit 031e5fb

Browse files
fix(cli): the per-package de-duplication key ignores the top-level collection index, so an echo no longer survives it (#18878)
Fixes #18779 ## The card's central reading, reproduced first Measured on `origin/main` `a43b9d0654` over the repo's own two-package fixture `examples/app-multi-package`, `os build --json` exiting 0: ``` warnings: 4 [0] field-no-consumers object "crm_order" · field "account" objects[0].fields.account [1] field-no-consumers object "crm_order" · field "amount" objects[0].fields.amount [2] field-no-consumers object "crm_account" · field "industry" objects[1].fields.industry [3] field-no-consumers package 'com.example.multi.core' — object "crm_account" · field "industry" objects[0].fields.industry ``` `[3]` is `[2]`, re-reported at the package-local index. Same rule, same entity, same message; the only difference is the top-level collection index, which is the one coordinate `findingKey` had no business comparing. **1 survivor, 1 echo, 0 genuinely new** — the card's reading holds, and the pass's strongest available value statement was carried entirely by a duplicate. ## Which option I took, and what the rejected one would have cost I fixed **the key**, and the comments with it. Fixing only the comments was the cheaper option and it was rejected on a measurement, not a preference: the de-duplication exists so that "the author cannot tell a real per-package finding from an echo" would stop being true, and on the one fixture anyone can check, it was still true. Correcting the prose would have left `os build` printing `4 author-time warning(s)` for 3 distinct ones, and left the repo with an accurate comment describing a filter that does not filter. The cost of the option I took is that `os build` / `os validate` / `os lint` report one fewer line on such a project, which is an observable change and is why this carries a changeset. Triage settled the blocker: the "`os build` output stays byte-identical" constraint was #18769's **PR contract, not a product contract**, and its scope ended with that PR. ## Measured: exactly what stops being reported Same fixture, all three doors, the key reverted and restored in place (on-disk blob hash asserted both ways, tree verified clean after): | | before | after | |---|---|---| | `os build --json` | warnings 4, exit 0 | warnings 3, exit 0 | | `os validate --json` | warnings 4, exit 0 | warnings 3, exit 0 | | `os lint --json` | total 4, failing 0, exit 0 | total 3, failing 0, exit 0 | | `os lint --json --strict` | total 4, failing 4, exit 1 | total 3, failing 3, exit 1 | The one line that stops being reported is the echo above. **No input's verdict moves**, and that is structural rather than a property of this fixture: every finding the de-duplication drops has, by construction, a finding with the same key already in the reported set — the seed is the union run's findings, which every door reports, and it grows only with per-package findings that themselves survived. `os build` already exits 1 on a union error *before* this pass runs, and `os lint --strict` fails on `errors + warnings`, a count that could only reach zero if the twin went unreported too. ## Clause-② Clause-②: no Derived from the measured diff, not inherited. The dispatching claim left this blank deliberately and expected `yes (widening)` on the reasoning "fewer findings reported ⇒ `--strict` refuses less". Measured, `--strict` does **not** refuse less: `failing` drops 4 to 3 and the verdict stays `exit 1`, because the dropped line's twin is still counted. No accept set moves in either direction, and the diff adds no schema key, closed-set member, published export or registry entry. Declaration carrier: `Clause-②-correction: 5723810598` on the card. ## The falsified sentence — all carriers, re-derived `git grep "set the union could not see"` on `a43b9d0654` finds more than the four the dispatch named. Source **and** tests, all corrected here: | file | treatment | |---|---| | `packages/cli/src/commands/compile.ts` | claim corrected, quote kept | | `packages/cli/src/commands/lint.ts` | claim corrected, quote kept | | `packages/cli/src/commands/validate.ts` | claim corrected, quote kept | | `packages/cli/src/utils/artifact-packages.ts` | claim corrected at the definition | | `packages/cli/test/lint-per-package-authoring-parity.test.ts` | claim corrected | | `packages/cli/test/lint-per-package-authoring-seam.test.ts` | claim corrected | | `packages/cli/test/validate-per-package-authoring-parity.test.ts` | claim corrected | | `packages/cli/test/validate-per-package-authoring-seam.test.ts` | claim corrected | Each keeps the sentence as a **quotation being corrected** rather than deleting it, so the next reader meets the correction where they would have met the claim. ## TWO pins were being held up by the echo Both are the same shape and both were repaired the same way — by giving the fixture a survivor the union genuinely cannot see, never by relaxing an assertion. **1. `validate-per-package-authoring-parity.test.ts`** (#18677). Its non-vacuity case went red: the planted fixture's only per-package survivor was the echo, so filtering it left the parity cases comparing two empty sets. **2. `build-text-face-advisory-count.test.ts`** (#18780) — found by CI, not locally, and the local gap was mine: `packages/cli`'s `test` script is a bare `vitest run` with **no `--project` filter**, so CI runs both projects, while I had run `--project unit` plus only the two integration files this diff touches. This file is in the integration project and was never collected. Its lit control asserts the fixture reaches the per-package pass and leaves a survivor: ``` AssertionError: expected 0 to be greater than 0 test/build-text-face-advisory-count.test.ts:239 > the fixture reaches the per-package pass and raises a survivor there ``` `bc_account.industry` had no consumer anywhere, so the union raised it too and the "survivor" was that finding re-reported at the package-local index. `orders` now owns the view that displays it. ⛔ `toBeGreaterThan(0)` is untouched — it is what stops #18780's equality pins from going vacuous. Both preconditions now additionally assert the survivor's **pedigree** (the field is named only behind the per-package prefix, and no union finding names it), so neither control can be silently re-lit by a duplicate. The `warnings: 4` reading in #18780's header is kept as the record of the defect it measured, with a note that the same fixture reports 3 since this change. ## What the key does not buy, measured rather than quoted The key becomes position-insensitive, **not** collision-proof: two entries that render the same `where` still share a key, exactly as they already did whenever their indices happened to match. That residue is measured, not sized by citing a neighbouring pin — which is the move this card exists to correct. `packages/lint/src/data-model-rule-where-slot.test.ts` holds something narrower than "every rule names its entity in `where`": it fails any rule that puts a **bare config path** in `where`. Measured instead over every example stack in this repo that parses today (`app-multi-package`'s built artifact, `app-crm`, `app-showcase`, `app-todo`): 45 registry rules, 103 findings, **103 distinct neutralised keys, 0 collisions**, on `a43b9d0654`. ## Verification - **Pin red before / green after.** `test/per-package-dedup-positional-echo.test.ts`, key reverted to base in place: `2 failed | 4 passed` (the ECHO and REAL_UNION cases). Key restored: `6 passed`. On-disk mutation proved by blob hash both ways; the test imports the mutated module through a relative source specifier, so no build sits between mutation and assertion. - **The control can fail.** An ablation widening the rewrite from the top-level index to *every* index turns exactly one case red — the NESTED control. The first draft of that control was built on a `field-no-consumers` twin and stayed **green** under the same ablation, so the fixture now carries a bare `unique: true` index to give the control a finding whose path really has a nested index. A control that cannot fail is decoration. - **Tier measured in both directions**, from the predicate rather than the filename: the new pin fires no integration signal and is absent from the derived integration population — UNIT tier, asserted by the file's own last case. - **Changeset decided by measuring the built `dist`**, with controls both ways: the rewritten key is present in `packages/cli/dist/utils/artifact-packages.js` and `files[]` ships `dist`; positive control `runPerPackageAuthoringRules` present; negative control (a test-only symbol) 0 hits. Published ⇒ changeset, graded `patch`. - `dispatch-gates.mjs`: **61 derived families, 61 run, 0 NOT-MEASURED, 0 UNRUN** (exit codes recorded, none is 3). - `pnpm lint` (`eslint . --no-inline-config`, whole repo, not narrowed): **exit 0** at `6a0a4df1b4`. - `pnpm --filter @objectstack/cli test` run **WHOLE** — no `--project`, no file list, exactly what CI runs: **267 files / 3485 tests passed, exit 0**. The same command at `15cc0db8d4` reproduced CI's red first: `3 failed | 264 passed (267)`, of which the one real assertion was #18780's lit control (the other two were this worktree lacking `packages/cli/dist`, which that pin refuses by name; cleared by building the package). - `pnpm --filter @objectstack/cli typecheck`: exit 0. `check:dual-build-cjs-loads` first returned exit 3 — its own text says "This is NOT a pass: nothing was measured", 8 packages had no `dist` in this worktree. I built them and re-ran it: exit 0. ## Acceptance notes - **Two pending changesets still assert the falsified sentence** and are **not** touched here: `.changeset/18677-validate-per-package-authoring-pass.md` and `.changeset/18778-lint-per-package-authoring-pass.md`. `check:empty-changeset` refuses a PR that modifies a changeset present on the merge base, and names this exact situation as its DELIBERATE CORRECTION class, whose remedy is "do NOT restore it; get it confirmed on the PR". That confirmation is the reviewing seat's to give, so the call is surfaced here rather than taken. The correction itself is published in this PR's own changeset, which lands in the same release. Note also that #18677's changeset says "After: both report 4", which this change makes read 3. - Derivation ran against a tree behind `origin/main` (the derivation's own staleness warning named `scripts/gen-sdui-manifest-node.mjs` among others); CI judges the merge. - Noted, not filed: `bin/run-dev.js` needs `TSX_TSCONFIG_PATH` pinned when invoked from an example directory, and says so itself in a good refusal — a working command, not a defect. Authored by Claude Code in session `session_01DvvamiacK328idtBYJBxV3` (durable attribution kept in prose: the body-edit channel appends its own footer block, so a footer sent here would be stored twice). --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0a5851d commit 031e5fb

11 files changed

Lines changed: 680 additions & 81 deletions
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
The per-package author-time de-duplication key ignores the top-level collection index, so a package-local finding no longer survives as an echo of the union finding it duplicates
6+
7+
`runPerPackageAuthoringRules` runs the author-time rule table once per
8+
`packages[]` entry and drops anything the union run already reported. Its key
9+
was `rule` + `where` + `path` + `message`, and `path` is **positional**: a
10+
package body re-bases every collection from 0, while the flattened union numbers
11+
that same entry wherever `authoringRuleUnionStack` placed it.
12+
`objects[0].fields.industry` and `objects[1].fields.industry` are ONE finding
13+
under two spellings, so the `Set` never matched them and the echo survived the
14+
filter that exists to remove it.
15+
16+
Measured on `origin/main` a43b9d0654 over the repo's own two-package fixture
17+
`examples/app-multi-package`, at every door, before and after:
18+
19+
| | before | after |
20+
|---|---|---|
21+
| `os build --json` | warnings 4, exit 0 | warnings 3, exit 0 |
22+
| `os validate --json` | warnings 4, exit 0 | warnings 3, exit 0 |
23+
| `os lint --json` | total 4, failing 0, exit 0 | total 3, failing 0, exit 0 |
24+
| `os lint --json --strict` | total 4, failing 4, exit 1 | total 3, failing 3, exit 1 |
25+
26+
The one warning that stops being reported is `field-no-consumers` on
27+
`crm_account.industry` re-reported at the package-local index — the union run's
28+
own finding, printed a second time. Its twin is still reported, which is why no
29+
verdict moves.
30+
31+
**No input's verdict changes, and that is structural rather than a property of
32+
this fixture.** Every finding the de-duplication drops has, by construction, a
33+
finding carrying the same key already in the reported set: the seed is the union
34+
run's findings, which every door reports, and it grows only with per-package
35+
findings that themselves survived. So a door's refusal cannot flip — `os build`
36+
already exits 1 on a union error before this pass runs, and `os lint --strict`
37+
fails on `errors + warnings`, a count that could only reach zero if the twin
38+
went unreported too.
39+
40+
Only the **top-level** index is neutralised. Nested positions (`.indexes[1]`,
41+
`.columns[0]`) address the author's own document and read identically in both
42+
views, so they stay in the key and keep discriminating. A finding's own `path`
43+
is never modified — every door still prints the location it always printed.
44+
45+
What this does **not** buy: the key becomes position-insensitive, not
46+
collision-proof. Two entries that render the same `where` still share a key,
47+
exactly as they already did whenever their indices happened to match. Measured
48+
over every example stack in this repo that parses today (`app-multi-package`'s
49+
built artifact, `app-crm`, `app-showcase`, `app-todo`), 45 registry rules
50+
produced 103 findings and 103 distinct neutralised keys — zero collisions.
51+
52+
Also corrected: the sentence "what survives the filter is exactly the set the
53+
union could not see", which was false for as long as the key was positional and
54+
had been copied from `compile.ts` into the `os validate` and `os lint` doors as
55+
each was wired. It is now stated at the bound the pass can actually hold, in
56+
every file that carried it.
57+
58+
Clause-②: no

‎packages/cli/src/commands/compile.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,22 @@ export default class Compile extends Command {
452452
// DE-DUPLICATED against the union run, because the union contains
453453
// every package's items: without this, a two-package project reports
454454
// every finding twice and the author cannot tell a real per-package
455-
// finding from an echo. What survives the filter is exactly the set
456-
// the union could not see.
455+
// finding from an echo.
456+
//
457+
// ⚠️ [#18779] This comment used to end "What survives the filter is
458+
// exactly the set the union could not see", and that was FALSE for
459+
// as long as the de-duplication key carried the POSITIONAL `path`:
460+
// a package body re-bases its collections from 0, so one finding got
461+
// two keys and its echo survived the very filter described here. The
462+
// sentence was quoted as authority by #18677 and #18778 without the
463+
// definition being opened, and copied into the `os validate` and
464+
// `os lint` doors as each was wired. `findingKey` now neutralises
465+
// the top-level collection index, so what survives is the set of
466+
// per-package findings no union finding already carried under the
467+
// same rule, `where`, message and non-top-level position. ⛔ Do not
468+
// re-inflate that to "exactly the set the union could not see" —
469+
// `utils/artifact-packages.ts` states the bound and why it is
470+
// narrower than that sentence.
457471
//
458472
// [#16611] Each package's stack is handed the artifact's `packages[]`
459473
// as RESOLUTION CONTEXT — see `packageBodyAsStack`. The list read here

‎packages/cli/src/commands/lint.ts‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -684,10 +684,19 @@ export function lintConfig(config: any, opts: LintConfigOptions = {}): LintIssue
684684
//
685685
// The second half of the run above, and the half THIS door ran without.
686686
// `os build` has run it since #16611 and `os validate` since #18677; `os
687-
// lint` ran the union fold and stopped. `compile.ts` step 3b-ii says what
688-
// survives the de-duplication is "exactly the set the union could not see" ⇒
689-
// that whole set was findings `os build` reported and this command
690-
// structurally could not.
687+
// lint` ran the union fold and stopped. Every finding this pass yields is
688+
// therefore one `os build` reported and this command structurally could not.
689+
//
690+
// ⚠️ [#18779] This paragraph used to size that gap by quoting `compile.ts`
691+
// step 3b-ii — "exactly the set the union could not see" — and that sentence
692+
// was FALSE when it was copied here: the de-duplication key carried the
693+
// POSITIONAL `path`, so a package-local finding and its flattened twin got
694+
// two keys and the ECHO survived. Part of every survivor set was therefore
695+
// something this door's own union run ALREADY reported. The key was
696+
// corrected in `utils/artifact-packages.ts`; the gap this door closed is
697+
// real and its direction is unchanged, but ⛔ do not re-derive its size from
698+
// that sentence — it was quoted, never measured, by the two cards that
699+
// wired the second and third doors.
691700
//
692701
// ⚠️ The reading that hid it for two cards is the one the imports above
693702
// invite: this file DOES call `artifactPackages` and `packageBodyAsStack` —
@@ -727,8 +736,10 @@ export function lintConfig(config: any, opts: LintConfigOptions = {}): LintIssue
727736
// walks. ⛔ Not `stack` — that would judge un-lowered package bodies here
728737
// and lowered ones there, which is #16095 one layer in.
729738
parsed: lowered,
730-
// De-duplicated against the run above, on the UNPREFIXED finding, so what
731-
// reaches the list below is the set the union could not see.
739+
// De-duplicated against the run above, on the UNPREFIXED finding — so what
740+
// reaches the list below is what that run did not already carry under the
741+
// same rule, `where`, message and non-top-level position (#18779; the key
742+
// used to compare the top-level index too, and let the echo through).
732743
unionFindings,
733744
sduiManifest: opts.sduiManifest,
734745
loweredHookRefs,

‎packages/cli/src/commands/validate.ts‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -391,13 +391,23 @@ export default class Validate extends Command {
391391
//
392392
// `os build` has run it since #16611; `os validate` ran the union
393393
// fold and stopped, importing neither `artifactPackages` nor
394-
// `packageBodyAsStack`. `compile.ts` step 3b-ii says what survives
395-
// the de-duplication is "exactly the set the union could not see" ⇒
396-
// that whole set was findings `os build` reported and this command
397-
// structurally could not. Same FALSE-CLEAN direction #17069 fixed one
398-
// layer up, and the worse door for it: the fast inner-loop check is
399-
// what an author runs BEFORE shipping, so its clean bill of health is
400-
// the strongest false assurance the three commands can give.
394+
// `packageBodyAsStack`. Every finding this pass yields is therefore
395+
// one `os build` reported and this command structurally could not.
396+
// Same FALSE-CLEAN direction #17069 fixed one layer up, and the worse
397+
// door for it: the fast inner-loop check is what an author runs
398+
// BEFORE shipping, so its clean bill of health is the strongest false
399+
// assurance the three commands can give.
400+
//
401+
// ⚠️ [#18779] This step used to size that gap by quoting `compile.ts`
402+
// step 3b-ii — "exactly the set the union could not see" — and that
403+
// sentence was FALSE when it was copied here: the de-duplication key
404+
// carried the POSITIONAL `path`, so a package-local finding and its
405+
// flattened twin got two keys and the ECHO survived. Part of every
406+
// survivor set was therefore something THIS door's own union run
407+
// already reported. The key was corrected in
408+
// `utils/artifact-packages.ts`; the gap this step closed is real and
409+
// its direction is unchanged, but ⛔ do not re-derive its size from
410+
// that sentence — it was quoted, never measured.
401411
//
402412
// ⛔ Not a second copy of the loop — `runPerPackageAuthoringRules` is
403413
// the one the build door calls, so the de-duplication key, the

‎packages/cli/src/utils/artifact-packages.ts‎

Lines changed: 100 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,62 @@ import {
4646
type AuthoringFinding,
4747
} from '@objectstack/lint';
4848

49+
/**
50+
* The leading `collection[N]` of a finding path — the ONE coordinate that
51+
* differs between the two views of a single finding. `objects[3].fields.x`
52+
* matches `objects[3]`; `manifest.namespace` matches nothing.
53+
*/
54+
const TOP_LEVEL_COLLECTION_INDEX = /^([A-Za-z_][A-Za-z0-9_]*)\[\d+\]/;
55+
4956
/**
5057
* Identity of one finding, for the per-package de-duplication below.
5158
*
52-
* Moved here from `compile.ts` unchanged (#18677): the two doors must
53-
* de-duplicate identically, or "the set the union could not see" means two
54-
* different things depending on which command the author happened to run.
59+
* Moved here from `compile.ts` unchanged (#18677); its POSITIONAL half was
60+
* corrected here (#18779). All three doors must de-duplicate identically, or
61+
* what survives the filter means a different thing depending on which command
62+
* the author happened to run.
63+
*
64+
* ## Why the top-level collection index is neutralised (#18779)
65+
*
66+
* `rule`, `where` and `message` say WHICH finding this is; `path` says where
67+
* it sits. The inherited key used `path` raw — and `path` is positional, so
68+
* one finding judged twice got two keys and the `Set` below never matched
69+
* them. A package body re-bases every collection from 0, while the flattened
70+
* union numbers that same entry wherever `authoringRuleUnionStack` placed it:
71+
* `objects[0].fields.industry` (package-local) and `objects[1].fields.industry`
72+
* (union) are ONE finding under two spellings. Measured on
73+
* `examples/app-multi-package` before this landed — 1 survivor, 1 echo, 0
74+
* genuinely new, and `os build` printed "4 author-time warning(s)" for 3
75+
* distinct ones. The de-duplication exists precisely so that "the author
76+
* cannot tell a real per-package finding from an echo" would stop being true,
77+
* and the positional key is why it stayed true.
78+
*
79+
* ⛔ The rewrite touches the KEY only — a finding's own `path` is never
80+
* modified, so every door still prints the positional location it always
81+
* printed. And only the TOP-LEVEL index: nested positions (`.indexes[1]`,
82+
* `.columns[0]`) address the author's own document and read identically in
83+
* both views, so they stay in the key and keep discriminating.
84+
*
85+
* ⛔ Not `nameKeyFindingPath` (`@objectstack/lint`'s runtime-gate rewrite of
86+
* this same coordinate), for the reason that function's own docblock records:
87+
* it is "Applied AFTER the differential, not before it … two stored items that
88+
* (illegitimately) share a name must not have their distinct findings merged
89+
* or cancelled by the rewrite". A de-duplication key IS that differential, so
90+
* name-keying is the one place it rules itself out. Two further readings from
91+
* the same docblock: its key set is DERIVED and holds `objects`, `permissions`
92+
* and `books` today, so it would leave every other collection's echo standing,
93+
* and it is "Exported for the pin, not for callers" — it sits on neither of
94+
* that package's entries.
95+
*
96+
* ⚠️ What this does NOT buy, written down so the next reader does not
97+
* re-inflate it: the key becomes position-insensitive, ⛔ not collision-proof.
98+
* Two entries that render the same `where` — an illegitimate duplicate name —
99+
* still share a key, exactly as they already did whenever their indices
100+
* matched too. The claim the pass below is entitled to make is stated there,
101+
* and it is narrower than "exactly the set the union could not see".
55102
*/
56103
const findingKey = (f: { rule: string; where: string; path: string; message: string }): string =>
57-
[f.rule, f.where, f.path, f.message].join('\u0000');
104+
[f.rule, f.where, f.path.replace(TOP_LEVEL_COLLECTION_INDEX, '$1[]'), f.message].join('\u0000');
58105

59106
/**
60107
* The artifact's package entries, as `{ index, id, body }` (ADR-0130 D4).
@@ -161,30 +208,55 @@ export function packageBodyAsStack(
161208
* ## What the asymmetry was, measured
162209
*
163210
* `os build` ran this pass; `os validate` ran the union fold and stopped,
164-
* importing neither seam above. `compile.ts`' own comment says what survives
165-
* the de-duplication is "exactly the set the union could not see" ⇒ that whole
166-
* set was findings `os build` reported and `os validate` structurally could
167-
* not. The direction is FALSE-CLEAN, and on the command an author runs BEFORE
168-
* shipping — the same direction and the same door #17069 fixed one layer up,
169-
* which is why `authoringRuleUnionStack` being in both commands did not settle
170-
* it. `packages/cli/test/build-json-advisory-parity.e2e.test.ts` already
171-
* asserted "nothing rides in build's `warnings` that validate does not also
172-
* report"; it stayed green because its fixture declares no `packages[]` at all,
173-
* so the pass it would have caught never ran there.
174-
*
175-
* ## The de-duplication key is the caller's, and it is not perfect
176-
*
177-
* `findingKey` below is `compile.ts`' key, moved unchanged: `rule`, `where`,
178-
* `path`, `message`. ⚠️ `path` is POSITIONAL, and a collection index in one
179-
* package's own body is not the index the flattened top level gives the same
180-
* item — so a finding on any package whose local index differs from its
181-
* flattened one survives the filter as an ECHO of a union finding rather than
182-
* as something the union could not see. Measured on `examples/app-multi-package`
183-
* (2 packages, `crm_account.industry`): 1 survivor, 0 of them new. ⛔ Not fixed
184-
* here — changing the key changes what `os build` reports, which is a separate
185-
* decision from making the two doors agree, and agreeing IMPERFECTLY at one
186-
* seam is strictly better than disagreeing at two. When it is fixed it is
187-
* fixed once, for both commands, which is the property this module buys.
211+
* importing neither seam above. Every finding this pass yields was therefore
212+
* one `os build` reported and `os validate` structurally could not — the
213+
* direction is FALSE-CLEAN, and on the command an author runs BEFORE shipping.
214+
* Same direction and same door #17069 fixed one layer up, which is why
215+
* `authoringRuleUnionStack` being in both commands did not settle it.
216+
* `packages/cli/test/build-json-advisory-parity.e2e.test.ts` already asserted
217+
* "nothing rides in build's `warnings` that validate does not also report"; it
218+
* stayed green because its fixture declares no `packages[]` at all, so the pass
219+
* it would have caught never ran there.
220+
*
221+
* ⚠️ #18779 corrected the SIZE that sentence used to be given, ⛔ not its
222+
* direction. #18677 and #18778 both sized this blind spot by quoting
223+
* `compile.ts`' claim that the survivors are "exactly the set the union could
224+
* not see" — but the key was positional, so part of every survivor set was
225+
* ECHO: findings the union run ALSO reported, which means `os validate` was
226+
* reporting them all along through its own union run. On
227+
* `examples/app-multi-package` the whole of it was — 1 survivor, 1 echo, 0
228+
* genuinely new — so `os validate`'s true blind spot on that fixture was ZERO
229+
* findings, not one. ⛔ Neither card measured that; both quoted it. The
230+
* asymmetry was real and worth closing on every door; its magnitude was
231+
* inherited from a sentence nobody had read the definition behind.
232+
*
233+
* ## What the de-duplication key can and cannot promise
234+
*
235+
* `findingKey` above neutralises the top-level collection index (#18779), so
236+
* the two views of one finding now produce one key and an echo is filtered.
237+
* What reaches the lists below is therefore the set of per-package findings
238+
* whose `rule`, `where`, `message` and NON-top-level position no union finding
239+
* already carried.
240+
*
241+
* ⚠️ That is the whole claim, and it is deliberately narrower than "exactly the
242+
* set the union could not see" — ⛔ do not restate it as that sentence. Two
243+
* entries rendering the same `where` still collapse (see `findingKey`), and a
244+
* rule that reports the same `rule`/`where`/`message` for genuinely different
245+
* items distinguished ONLY by their top-level index would collapse with them.
246+
*
247+
* ⛔ And do not size that residue by quoting the pin next door — that move is
248+
* exactly what this card exists to correct. `packages/lint/src/
249+
* data-model-rule-where-slot.test.ts` holds something NARROWER than "every
250+
* rule names its entity in `where`": it runs the whole registry and fails any
251+
* rule that puts a BARE CONFIG PATH in `where`. That forbids the one spelling
252+
* which would make the collapse systematic; it does ⛔ not promise that two
253+
* entries always render different `where` strings. So the residue is MEASURED
254+
* instead — over every example stack in this repo that parses today
255+
* (`app-multi-package`'s built artifact, `app-crm`, `app-showcase`,
256+
* `app-todo`), 45 registry rules produced 103 findings and 103 distinct
257+
* neutralised keys: ZERO groups held two different raw paths. ⛔ Re-measure
258+
* rather than re-quote that number — a corpus reading is a count plus the tree
259+
* it was taken against, and this one was taken on a43b9d0654.
188260
*/
189261
export function runPerPackageAuthoringRules(run: {
190262
/** Which door is asking — the same string its union run passed. */

0 commit comments

Comments
 (0)