Skip to content

Commit 9bd631f

Browse files
fix(cli): os lint runs the per-package author-time rule pass the other two doors already ran (#18813)
Fixes #18778 Clause-②: yes (narrowing) `os lint --strict` now exits 1 on a project it exited 0 for. A newly-refused input is **exhibited** below, not reasoned about. ⚠️ #18677's `Clause-②: no` rested on "no newly-refused input could be exhibited" measured on `os validate`'s door; ⛔ it does not transfer, and on this door it is false. Verified through `readClause2Line` from `scripts/pm/check-clause2-carriers.mjs`, ⛔ not an ad-hoc regex. `os build` has run the author-time rule table **twice** since #16611 — once over the union-folded stack, then once per `artifactPackages(…)` entry with `packageBodyAsStack(…)` as resolution context, de-duplicated against the union run. #18769 gave `os validate` the second half. `os lint` ran the union fold and stopped, so by `compile.ts`' own description the survivors of that pass — *"exactly the set the union could not see"* — were findings `os build` reported and `os lint` **structurally could not**. Same false-clean direction, on the fastest of the three doors. All three now call the one shared pass. ## ⭐ The trap, confirmed rather than relayed `lint.ts` **does** import `artifactPackages` and `packageBodyAsStack` — at `packages/cli/src/commands/lint.ts:18`, used at `:542` and `:546`. Opening the hits is what settles it: they feed `os lint`'s **own** intra-package duplicate-name advisory (#17821), never the shared rule table. ⛔ A count is not a reading. This door is also the one place in the tree where "the loop is already written here, write the second one beside it" is the cheap move, so the seam pin ratchets the call **count** rather than its absence — the sibling doors' `not.toMatch(/packageBodyAsStack\(/)` assertion ⛔ cannot be transplanted here. ## Should `os lint` run it? — settled from the repo's own statements, ⛔ not assumed The dispatch fenced this as a contract question with a STOP arm. It did not need the stop arm; four statements settle it, and the one statement that reads the other way is **false on the tree**. **For, and they are this door's own words:** 1. `packages/lint/src/authoring-rules.ts:38-44` — *"Any rule that can emit `error` runs on all three commands. A gate is only as strong as the weakest command an author or CI happens to run, so a gating rule with partial coverage is not a stricter check — it is a coin flip"*, and *"the three commands are three doors in ONE wall"*. 2. `packages/cli/src/commands/lint.ts:652-656` — the union fold landed **here**, for this exact direction: *"the whole table reported nothing and `os lint` returned no finding of any severity for a project `os build` refuses"*. That is the same sentence as this card, one layer out; the fold fixed which COLLECTIONS the table sees, this fixes which STACKS it is run over. 3. `packages/cli/src/commands/lint.ts:604-611` — `os lint`'s pre-registry hand-wired subset was removed because *"a pre-flight that disagrees with the gate in both directions is worse than no pre-flight"*. 4. `packages/cli/test/validate-build-gate-parity.test.ts` already holds the INPUT equal across all three doors (`all three authoring commands hand the rule table the union-folded stack`), and its #12297 note states the governing rule for exactly this shape: *"A guard that enumerates a subset of the class it describes reports green for the members it forgot. The list is the class now, not the card."* **Against — one statement, and ⚠️ it was already FALSE when it was written:** `validate-build-gate-parity.test.ts`' `PARITY_COMMANDS` docblock read *"`lint.ts` … emits no artifact and **runs no artifact-level gate**, so it is not part of the parity question."* Measured at `7572329069`: `lint.ts:13` imports and `lint.ts:881` **calls** `collectAndLintDocs` — a name in that same file's `SHARED_NON_REGISTRY_GATES` roster, i.e. an artifact-level gate by the file's own classification. The clause is corrected in this PR rather than deleted, because it is the one sentence in this repository that could be read as "`os lint` is exempt from the artifact-level gates" and this card had to settle exactly that. What excludes `lint.ts` from `PARITY_COMMANDS` is the ARTIFACT (that file's question is `os validate` as `os build`'s read-only superset), not the gates. ⛔ `PARITY_COMMANDS` is deliberately NOT widened — that would re-open every ledger classification against a third file in one stroke. ⇒ the roster prediction in the dispatch resolved the other way: `SHARED_NON_REGISTRY_GATES` and its `it.each('both commands run %s')` did **not** move. They are keyed to `PARITY_COMMANDS`, which this PR leaves at two files, so nothing on that roster reddened. What moved is the docblock the roster is read through. ## The measurement Fixture `CONFIG_FLIP` (shipped as the pin's own fixture): `core` owns `pp_account`, the sibling `orders` package owns the view that displays `pp_account.industry`. Judged as one flattened union the field has a consumer and nothing is raised; judged per package, `core` declares a field nothing in `core` reads. ⇒ **the union run is clean and the per-package run is not** — the only shape that can tell "the doors agree" from "the doors agree because neither looked". At `origin/main` `7572329069`: | | `os build --json` | `os lint --json` | `os lint --json --strict` | |---|---|---|---| | **before** | warnings **1** (per-package only) | total **0**, exit **0** | **exit 0**, `failing: 0` | | **after** | warnings **1**, unchanged | total **1**, exit **0** | **exit 1**, `failing: 1` | On the sibling two-package fixture from #18769 (`CONFIG_MULTI`, where the survivor is the positional-key ECHO): `os build` 3 warnings · `os validate` 3 · `os lint` **2 before, 3 after** — the three doors now report one set. **CONTROL** — a single-package project (no `packages[]`): `packageCount` 0, the pass is skipped, and `os lint` reports zero per-package findings before and after. Without it "the doors agree" is satisfied by three commands that all looked at nothing, which is exactly how this gap survived two cards' worth of parity files. ⛔ **The DEFAULT face is not claimed to move.** No `error`-severity per-package-only finding was exhibited: two probe shapes were tried — a cross-package field consumer and a cross-package page reference (`nav-target-unresolved`) — and both land at `warning`. That half is **NOT MEASURED**, and the `--strict` pin says so in its own comment rather than asserting an unmeasured default-face flip. The severity face is `os lint`'s own and unchanged: one mapping expression now serves both halves (`info` → `suggestion`, everything else verbatim), so an `error` fails the run, a `warning` fails it only under `--strict`. A per-package `error` is one `os build` ALREADY refuses, so this narrows `os lint` to the bar the command that ships holds and never past it. ## Red/green, both legs, from the committed state `packages/cli/src/commands/lint.ts` alone restored to its `7572329069` blob (`git show 7572329:…`), everything else at HEAD. Marker count on disk `2 → 0` **verified before running**; restored with `git checkout HEAD -- <path>`, `git diff HEAD` empty and `git hash-object` back to `5b2eb1af023348865d06a4ab7355708af801db43` after each leg. | pin | ablated | at HEAD | |---|---|---| | `test/lint-per-package-authoring-seam.test.ts` (unit) | **2 failed**, 4 passed | **6 passed** | | `test/lint-per-package-authoring-parity.test.ts` (integration) | **2 failed**, 3 passed | **5 passed** | The 4 and 3 that pass in both legs are deliberately door-independent (the pass's own three-door equality, the `findings` ∪ split identity, the single-package control, the `packageBodyAsStack` count ratchet) — they are not measuring `lint.ts`, and a file where everything reddened would mean the pins were coupled to the fix rather than to the behaviour. The ablated integration failure is the narrowing itself: `expected +0 to be 1` on `failing`. ## Tier, read from the config's own answer both directions `vitest list --filesOnly` per project, ⛔ not the predicate applied by hand: | file | `--project unit` | `--project integration` | |---|---|---| | `lint-per-package-authoring-seam.test.ts` | **1** | 0 | | `lint-per-package-authoring-parity.test.ts` | 0 | **1** | | `validate-per-package-authoring-seam.test.ts` (control) | **1** | 0 | | `validate-per-package-authoring-parity.test.ts` (control) | 0 | **1** | Populations non-vacuous: 214 unit files / 50 integration files. Neither new file constructs `new ObjectQL(`, so neither carries the KERNEL signal, and no existing file changed tier (no existing test file's source was touched except `validate-build-gate-parity.test.ts`, comment-only, which stays unit). ## What ran - `pnpm --filter '@objectstack/cli^...' build` — dependency closure, exit 0. - `pnpm --filter @objectstack/cli exec vitest run --project unit` — **214 files / 3047 tests, all pass**. - `pnpm --filter @objectstack/cli exec vitest run --project integration` over the **declared-narrowed** set of 9 files this diff can reach (`authoring-rule-command-parity`, `union-fold-command-parity`, `validate-per-package-authoring-parity`, `lint-per-package-authoring-parity`, `info-detail-package-fold`, `lint-eval-generator-refusal-separator`, `emit-json-pipe`, `adr-0048-app-split`, `nav-contribution-groups.package-id`) — **9 files / 53 tests, all pass**. The other 41 integration-tier files are DB/migration/secret/generate suites that never run the authoring rule table; ⇒ declared to CI. - `pnpm --filter @objectstack/cli typecheck` — exit 0, including `check:test-typecheck`. Both new test files are really in that program: `tsc -p tsconfig.test.json --listFiles` names them (2 of 2), so the claim is measured, not assumed. - Gate families derived from the merge base by `scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (⛔ not a hand-made path list): **61 derived, 58 run green, 3 NOT MEASURED, 0 UNRUN**, reconciled through `--ran` with an exit code recorded per family. The three are `check:dual-build-cjs-loads`, `check:i18n`, `check:i18n-coverage`, each **exit 3 = PREREQUISITE NOT MET** (packages with no `dist/` in this worktree) — ⛔ read as NOT MEASURED, never as a pass. `check:i18n-walk-parity` also refused at first for the same reason and went green after `pnpm --filter @objectstack/cli build`. - `eslint --no-inline-config` narrowed to the 5 changed source files: **0 errors, 0 warnings**, file count **5** read from `--format json`, not guessed. The narrowing is a measurement because the population is read from eslint's own config (`files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` plus per-directory overlays) and because `eslint.config.mjs:326-329` states this repo *"never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file"* — so this diff cannot move the verdict on any file it does not touch. The repo-wide `pnpm lint` run is CI's. ## Changeset `minor` on `@objectstack/cli` with a `**BREAKING**` banner and the ADR-0087 disposition `not-required (no-migration-prescription)` — `check:adr-0087-registration` reads the arm and prints it back: `[BREAKING+clause-②-narrowing]`. ⛔ Not `skip-changeset`: `packages/cli` is released and both edited sources ship in its `files[]`. `major` is out of the launch window, which `check:changeset-no-major` confirms green. ## Acceptance notes Noted, not filed — nothing here is a reproducible defect, a declared-contract violation, or a metadata trap: 1. ⚠️ **#18769's own `Clause-②: no` is falsified by this card's fixture, and the falsification is MEASURED.** `os validate --strict` exits 1 when `warnings > 0` (`validate.ts:715`). On `CONFIG_FLIP`, with `validate.ts` alone restored to `095c7f60ae^` it exits **0** (0 warnings); at HEAD it exits **1** (1 per-package warning). Restore verified by blob hash. ⇒ a newly-refused input **could** be exhibited on that door too; what was missing was a fixture whose union run is clean, and #18769's fixture (an ECHO of a union finding) structurally cannot be one. ⛔ Not corrected here — #18769 is merged, and rewriting a landed declaration is not this card's act. Carrier: the PM seat. Dedupe words: `18769 clause-② narrowing falsified` · `validate --strict per-package warning exit` · `union-clean per-package fixture`. 2. The `findings` member added to `runPerPackageAuthoringRules` exists because `os lint` has no severity split to re-join; re-joining `errors` then `advisories` at that door would put all errors before all advisories and silently re-order a list the union run above it produces in rule order. Stated in the member's own docblock so the next door does not have to re-derive it. 3. `packages/cli/vitest-tiers.ts` treats `new ObjectQL(` as a KERNEL signal, and a per-package pin is exactly the kind of test that grows one later. Nothing to file; the tier table above is the reading that would catch it. ⛔ Untouched, each with its own card: **#18779** (the positional de-duplication key — changing it changes what `os build` reports) and **#18780** (`os build`'s text face counting advisories it never prints). --- _Generated by [Claude Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 55523fd commit 9bd631f

6 files changed

Lines changed: 729 additions & 25 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
`os lint` runs the per-package author-time rule pass the other two doors already ran
6+
7+
`os build` has run the author-time rule table a second time, once per
8+
`packages[]` entry with that package's body as the stack and the artifact's own
9+
`packages[]` as resolution context, since #16611; `os validate` joined it in
10+
#18677. `os lint` ran the union fold and stopped, so every finding that pass
11+
produces — "exactly the set the union could not see", in the build command's own
12+
words — was reported by the command that ships and invisible on the fastest of
13+
the three doors. All three now call the one shared pass.
14+
15+
Measured on a two-package project whose union run is clean and whose per-package
16+
run is not (one package owns an object, a sibling package owns the view that
17+
displays its field):
18+
19+
| | before | after |
20+
|---|---|---|
21+
| `os build --json` | warnings 1 | warnings 1 |
22+
| `os lint --json` | total 0, exit 0 | total 1, exit 0 |
23+
| `os lint --json --strict` | exit 0 | exit 1 |
24+
25+
**BREAKING** — `os lint --strict` can now fail a project it passed before. A
26+
per-package finding is a finding this door could not see, `--strict` is
27+
documented as "treat warnings as errors", and the verdict moves with it. The
28+
default face is unchanged in the measurement above, and the severity mapping is
29+
`os lint`'s own: an `error` fails the run, a `warning` fails it only under
30+
`--strict`, an `info` stays a suggestion. Nothing is refused here that `os build`
31+
does not already refuse, so the pre-flight is narrowed to the bar the command
32+
that ships already holds and never past it. A run that must keep its old verdict
33+
drops `--strict`; a project that wants to keep it fixes what the pass reports,
34+
which is the same thing `os build` has been reporting all along.
35+
36+
Clause-②: yes (narrowing)
37+
38+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes changes: no spec key, export, config field or payload key is removed, renamed or added. What moved is which stacks one CLI command's existing rule table is run over, so `objectstack migrate meta` has nothing to rewrite and the ledger has nothing to record. -->

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

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import { scoreMetadata } from '../lint/score.js';
1515
import { checkHookBodyLowering } from '../lint/hook-body-lowering.js';
1616
import { lowerCallables } from '../utils/lower-callables.js';
1717
import { authoringRuleUnionStack } from '../utils/stack-collections.js';
18-
import { artifactPackages, packageBodyAsStack } from '../utils/artifact-packages.js';
18+
import {
19+
artifactPackages,
20+
packageBodyAsStack,
21+
runPerPackageAuthoringRules,
22+
} from '../utils/artifact-packages.js';
1923
import { runMetadataEval } from '../lint/metadata-eval.js';
2024
import { DEFAULT_METADATA_EVAL_CORPUS } from '../lint/corpus.js';
2125
import {
@@ -666,15 +670,73 @@ export function lintConfig(config: any, opts: LintConfigOptions = {}): LintIssue
666670
// so `lowered` already carries the folded collections and re-folding it here
667671
// would be a second call that could only ever return by identity.
668672
const { lowered, loweredHookRefs } = lowerCallables(stack as Record<string, unknown>);
669-
for (const f of runAuthoringRules('lint', {
673+
const unionFindings = runAuthoringRules('lint', {
670674
normalized: stack,
671675
parsed: lowered,
672676
sduiManifest: opts.sduiManifest,
673677
// [#16546] Same ref set `os build` computes from the same normalized
674678
// input — what lets `validateReadonlyHookWrites` / `validateHookBodyWrites`
675679
// report `hooks[i].handler` here byte-identically to `os build`.
676680
loweredHookRefs,
677-
})) {
681+
});
682+
683+
// ── The SAME rule table, once per PACKAGE (ADR-0130 D4, #18778) ──
684+
//
685+
// The second half of the run above, and the half THIS door ran without.
686+
// `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.
691+
//
692+
// ⚠️ The reading that hid it for two cards is the one the imports above
693+
// invite: this file DOES call `artifactPackages` and `packageBodyAsStack` —
694+
// for the intra-package duplicate-name advisory (#17821), which is `os
695+
// lint`'s OWN rubric and not the shared table. ⛔ A count is not a reading.
696+
//
697+
// ⛔ Not a second copy of the loop. This file already runs ONE per-package
698+
// walk of its own (the advisory above), so "write the loop here, it is
699+
// already the shape" is the live temptation at this door specifically — and
700+
// it is the one `utils/artifact-packages.ts`' header forbids by name: what
701+
// drifts between two hand-written loops is the VERDICT (the de-duplication
702+
// key, the severity split, the `where` prefix), not the package reading.
703+
//
704+
// Settled from the repo's own statements, ⛔ not assumed: `authoring-rules.ts`
705+
// calls the three commands "three doors in ONE wall" and holds a gate to its
706+
// weakest door; the union fold landed HERE (#17069/#17528) for this exact
707+
// false-clean direction, in this file's own words — "the whole table reported
708+
// nothing and `os lint` returned no finding of any severity for a project
709+
// `os build` refuses"; and the pre-registry hand-wired subset was removed
710+
// because "a pre-flight that disagrees with the gate in both directions is
711+
// worse than no pre-flight". This is that same sentence, on the INPUT.
712+
//
713+
// The severity face is `os lint`'s own and is unchanged: a per-package
714+
// finding is mapped by the same expression the union findings are, so an
715+
// `error` fails the run, a `warning` fails it under `--strict` and an `info`
716+
// stays a suggestion. ⛔ No severity judgement is made here — a per-package
717+
// `error` is one `os build` ALREADY refuses, so this narrows `os lint` to the
718+
// bar the command that ships holds, never past it.
719+
//
720+
// Skipped entirely for a stack with no `packages[]` (`packageCount` 0): one
721+
// package by definition, already judged whole by the union run above.
722+
const perPackageFindings = runPerPackageAuthoringRules({
723+
command: 'lint',
724+
// The LOWERED view, exactly as the `parsed` tier above is handed it and as
725+
// both other doors hand their parse: `lowerCallables` re-maps
726+
// `packages[*].manifest`, so this is the same per-package body `os build`
727+
// walks. ⛔ Not `stack` — that would judge un-lowered package bodies here
728+
// and lowered ones there, which is #16095 one layer in.
729+
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.
732+
unionFindings,
733+
sduiManifest: opts.sduiManifest,
734+
loweredHookRefs,
735+
}).findings;
736+
737+
// ⛔ ONE mapping for both halves. A second copy of this expression is how one
738+
// list comes to render `info` as `suggestion` and the other does not.
739+
for (const f of [...unionFindings, ...perPackageFindings]) {
678740
issues.push({
679741
severity: f.severity === 'info' ? 'suggestion' : f.severity,
680742
rule: f.rule,

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

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
* {@link runPerPackageAuthoringRules} — for the same reason one layer out: the
3030
* `os build` door ran it and the `os validate` door did not, and a second copy
3131
* of the loop is how that asymmetry would come back.
32+
*
33+
* ⚠️ #18778 measured that the asymmetry was 2 of 3 doors, not 1 of 2: `os lint`
34+
* ran the union fold and stopped as well, and the reading that hid it is the
35+
* one this module's own header invites — `lint.ts` DOES import both seams
36+
* above, for its intra-package duplicate-name advisory (#17821), so a sweep
37+
* that scores a door by symbol presence scores it as covered. ⛔ A count is not
38+
* a reading: what matters is which loop the symbols feed. All three doors call
39+
* the pass now.
3240
*/
3341

3442
import {
@@ -132,20 +140,23 @@ export function packageBodyAsStack(
132140

133141
/**
134142
* The author-time rule table, run ONCE PER PACKAGE and de-duplicated against a
135-
* union run — the pass `os build` has run since #16611 and `os validate` did
136-
* not (#18677).
143+
* union run — the pass `os build` has run since #16611, `os validate` did not
144+
* (#18677) and `os lint` did not (#18778).
137145
*
138-
* ## Why it lives here and not in one of the two commands
146+
* ## Why it lives here and not in one of the commands
139147
*
140148
* It is the THIRD entry to owe the shape the module header describes, and the
141149
* header's fence binds it: the only ways to reach `compile.ts`' loop from
142-
* `validate.ts` are to import one oclif command from another — pulling the
143-
* lowerer and the docs sweep into every `os validate` invocation — or to write
144-
* a second copy. ⛔ The second copy is what must not happen, and here it would
145-
* not be the `{index,id,body}` reading that drifted but the VERDICT: two loops
146-
* choosing their own de-duplication key, their own severity split or their own
147-
* `where` prefix is how one door comes to report a different set from the other
148-
* while both look right. That is the defect #18677 is, one layer down.
150+
* another command are to import one oclif command from another — pulling the
151+
* lowerer and the docs sweep into every `os validate` / `os lint` invocation —
152+
* or to write a second copy. ⛔ The second copy is what must not happen, and
153+
* here it would not be the `{index,id,body}` reading that drifted but the
154+
* VERDICT: two loops choosing their own de-duplication key, their own severity
155+
* split or their own `where` prefix is how one door comes to report a
156+
* different set from another while both look right. That is the defect #18677
157+
* is, one layer down — and #18778 is the measurement that the fence held: the
158+
* third door reached the SAME pass, and nothing about the pass moved but the
159+
* shape it hands back.
149160
*
150161
* ## What the asymmetry was, measured
151162
*
@@ -187,14 +198,30 @@ export function runPerPackageAuthoringRules(run: {
187198
}): {
188199
/** How many package entries were walked — 0 means the pass did not run. */
189200
packageCount: number;
201+
/**
202+
* Every surviving finding, `where`-prefixed, in walk order — the SAME set as
203+
* `errors` ∪ `advisories`, not a second computation of it (#18778).
204+
*
205+
* The two doors that hold an ARTIFACT to the bar need the severity SPLIT:
206+
* an `error` refuses the run and an advisory rides the warnings list, so
207+
* `compile.ts` and `validate.ts` read the two arrays below. `os lint` has no
208+
* such split — it maps every finding of every severity onto ONE `issues`
209+
* list through its own `info` → `suggestion` face and lets `--strict` decide
210+
* what fails — so re-joining the halves at that door would put all errors
211+
* before all advisories and silently re-order a list the union run above it
212+
* produces in rule order. This member is that door's shape, produced by the
213+
* one loop rather than by a caller stitching the halves back together.
214+
*/
215+
findings: AuthoringFinding[];
190216
errors: Array<{ package: string } & AuthoringFinding>;
191217
advisories: AuthoringFinding[];
192218
} {
193219
const artifactPackageEntries = run.parsed.packages;
194220
const packageEntries = artifactPackages(run.parsed);
221+
const findings: AuthoringFinding[] = [];
195222
const errors: Array<{ package: string } & AuthoringFinding> = [];
196223
const advisories: AuthoringFinding[] = [];
197-
if (packageEntries.length === 0) return { packageCount: 0, errors, advisories };
224+
if (packageEntries.length === 0) return { packageCount: 0, findings, errors, advisories };
198225

199226
const alreadyReported = new Set(run.unionFindings.map(findingKey));
200227
for (const pkg of packageEntries) {
@@ -206,13 +233,19 @@ export function runPerPackageAuthoringRules(run: {
206233
loweredHookRefs: run.loweredHookRefs,
207234
}).filter((f) => !alreadyReported.has(findingKey(f)));
208235
for (const f of pkgFindings) alreadyReported.add(findingKey(f));
236+
// ⛔ ONE prefixer, applied to every member of all three lists. The `where`
237+
// prefix is the only thing that identifies a finding as per-package on any
238+
// door's face, and a second spelling of it is the drift this module exists
239+
// to foreclose — one layer smaller than the second copy of the loop its
240+
// header forbids, and invisible in exactly the same way.
241+
const prefixed = (f: AuthoringFinding): AuthoringFinding => ({
242+
...f,
243+
where: `package '${pkg.id}' — ${f.where}`,
244+
});
245+
findings.push(...pkgFindings.map(prefixed));
209246
const split = splitBySeverity(pkgFindings);
210-
advisories.push(
211-
...split.advisories.map((a) => ({ ...a, where: `package '${pkg.id}' — ${a.where}` })),
212-
);
213-
errors.push(
214-
...split.errors.map((e) => ({ ...e, package: pkg.id, where: `package '${pkg.id}' — ${e.where}` })),
215-
);
247+
advisories.push(...split.advisories.map(prefixed));
248+
errors.push(...split.errors.map((e) => ({ ...prefixed(e), package: pkg.id })));
216249
}
217-
return { packageCount: packageEntries.length, errors, advisories };
250+
return { packageCount: packageEntries.length, findings, errors, advisories };
218251
}

0 commit comments

Comments
 (0)