Skip to content

refactor(output): sort rule violations once, in the pipeline (#147) - #195

Merged
Gallevy merged 1 commit into
mainfrom
advisor/147-sort-once
Sep 13, 2026
Merged

Gallevy merged 1 commit into
mainfrom
advisor/147-sort-once

Conversation

@Gallevy

@Gallevy Gallevy commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Closes #147. Follow-up to #194.

What

sortViolationsBySeverity was called by three renderers, each re-sorting the same list at render time:

  • printRules (src/utils/print-rules.ts)
  • buildRulesSection for --summary-file (src/utils/write-summary-file.ts)
  • printJson (src/utils/print-json.ts)

aggregated.ruleViolations itself stayed in detection order, so "the order of ruleViolations" wasn't one fact about the report — it was three that agreed by convention. A fourth consumer added later would silently reintroduce #87's bug for its own surface.

Now it's sorted once, at the single point the report is assembled, and every renderer reads the field directly.

#147 hedged on where the sort should go — "in aggregateReports … or in runPipeline" — because at the time there was no single assembly point. #194 created one, so this lands at runPipeline's single AggregatedReport construction and the hedge resolves itself.

The one real decision

Plugin findings are appended before the sort rather than after it, so they interleave by severity like any other violation — a plugin's error is not ranked below hermex's info merely for having been produced last.

That's a change to what aggregated.ruleViolations means, even though no current output moves (every renderer already sorted the combined list). It's a decision rather than a side effect of where the sort landed, so it's stated in a comment at the call site and pinned by a test.

Verification

  • pnpm run test:output: all 30 cases match main.
  • 1205 tests pass (59 files); lint, format, typecheck, build all exit 0.
  • The new e2e guard was confirmed to fail without the fix — reverting the sort produces warn → info → error and the test catches it.

The guard needed a new fixture. The existing plugin fixture adds its findings in error → warn → info order, so detection order and severity order already agreed there and the assertion would have passed against unsorted code. tests/e2e/hermex-plugin-ordering.config.ts makes them disagree on purpose: hermex contributes warn then info, the plugin contributes the only error, last.

Consumers that read the list raw

Checked before changing the order underneath them, since this is where an upstream sort could have changed behaviour silently:

Consumer Affected?
printPackages → collectPackageFlags No — both contributors .find() the list, but no-packages breaks after the first match per package and no-deprecated-packages emits one per package, so there is nothing to pick between
buildRulesSection (--summary-file) No — it filters info out, and filter-then-sort agrees with sort-then-filter for a stable sort
computeCompliance → groupBySeverity No — bucketing, not ordering
Plugins (PluginInventoryView.violations) No — runPluginPhase receives the pre-assembly list, so plugins still see detection order

sortViolationsBySeverity stays exported for a caller assembling its own list; its doc comment previously asserted the opposite invariant ("render-time only") and has been corrected.

🤖 Generated with Claude Code

`sortViolationsBySeverity` was called by three renderers — printRules,
buildRulesSection for --summary-file, and printJson — each re-sorting the same
list at render time. `aggregated.ruleViolations` itself stayed in detection
order, so "the order of ruleViolations" was three separate facts that agreed
only by convention, and a fourth consumer added later would silently
reintroduce #87's bug for its own surface.

Sort once, at the single point the report is assembled (#84 made there be
one), and let every renderer read the field directly.

Plugin findings are appended before the sort rather than after it, so they
interleave by severity like any other violation: a plugin's error is not
ranked below hermex's info merely for having been produced last. That is a
decision, not a side effect, and it is now pinned by a test.

No output change: all 30 output-review cases match main. The new e2e guard
was confirmed to fail without the fix — its fixture makes detection order and
severity order disagree, which the existing plugin fixture could not do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ No Changeset found

Latest commit: cdd9a82

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Output Review

0 of 30 case(s) changed · 0 invariant breach(es) · all 30 cases

Reference: ef8369c — reused from cache.

Output is unchanged. Nothing to review.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 98.59%
⬇️ -0.3%
1749 / 1774
🟢 Statements 98.14%
⬇️ -0.3%
1955 / 1992
🟢 Functions 97.86%
⬇️ -1.0%
413 / 422
🟢 Branches 97.02%
⬇️ -0.2%
1339 / 1380
File Coverage
File Stmts Branches Functions Lines
Changed Files
src/rules/run.ts 62.5%
🟰 ±0%
50%
🟰 ±0%
100%
🟰 ±0%
62.5%
🟰 ±0%
src/utils/severity-format.ts 88.09%
⬇️ -11.9%
92.3%
⬇️ -7.7%
69.23%
⬇️ -30.8%
86.48%
⬇️ -13.5%
src/utils/print-rules.ts 93%
🟰 ±0%
91.35%
🟰 ±0%
100%
🟰 ±0%
94.56%
🟰 ±0%
src/utils/print-json.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/utils/write-summary-file.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
Generated in workflow #250 for commit cdd9a82 by the Vitest Coverage Report Action

@Gallevy

Gallevy commented Sep 13, 2026

Copy link
Copy Markdown
Owner Author

Opened #196 to get a second opinion on the plugin-interleaving decision in this PR, rather than leaving it in a code comment and one test.

Two things it records that reviewers of this PR should see:

  1. The asymmetry. Plugins still see detection order (runPluginPhase gets the pre-assembly list), while every downstream consumer now sees severity order. PluginInventoryView.violations is public API, so a reporter plugin emits a differently-ordered list than hermex scan --format json does for the same run. Decide whether plugin findings should interleave by severity — and whether plugins should see the same order #196 lays out three ways to resolve it; my preference is to sort what plugins see too.

  2. A caveat on this PR's own verification. "All 30 output-review cases match main" is true but does not cover the plugin path — none of the cases in fixtures/cases.ts configures a plugin. That path is covered by the e2e suite and the new ordering guard, not by output review. Decide whether plugin findings should interleave by severity — and whether plugins should see the same order #196 suggests adding a scan-plugins case to close the gap.

Neither changes what this PR does; both are things I'd rather have on the record than assumed.

@Gallevy
Gallevy merged commit 022312b into main Sep 13, 2026
6 checks passed
@Gallevy
Gallevy deleted the advisor/147-sort-once branch September 13, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rule violations are sorted 3 times, once per renderer — sort once in the pipeline instead

1 participant