Skip to content

Group lint output by file with aligned rows, deduped messages, and --statistics #413

Description

@not-stbenjam

The default text formatter prints one flat list per severity in rule-execution order: a single file's problems are scattered across non-adjacent clusters, the full path is repeated on every line, and verbatim-identical messages are never collapsed. This proposes the layout every comparable linter (eslint, ruff) settled on — group by file with one header per file and aligned line / severity / rule / message rows, dedup repeated identical messages, and add a ruff-style --statistics flag for per-rule counts. Text output only; JSON/SARIF/HTML are untouched.

Text is the default format every user sees, and the payoff lands hardest during onboarding, when violation counts are highest and the flat list is at its worst.

Why

Verified against main (5d2dbf9, 2026-07-06):

  • src/skillsaw/formatters/text.py (lines 45–72) emits three flat per-severity lists in rule-execution order — no file grouping, no sort, no dedup, full relative path repeated on every row.
  • Self-linting this repo shows the cost concretely: 70 info violations shown (74 found, 4 baseline-suppressed); skills/skillsaw-onboard/SKILL.md appears on 8 non-adjacent lines across 3 rule clusters; one verbatim-identical terminology message repeats 11 times.
  • No --statistics flag exists anywhere (src, docs, README).
  • Partial precedent already in-tree: src/skillsaw/formatters/html.py:46 sorts violations by (severity, file, line) — but HTML only, and without file headers.

Proposed implementation

  1. Rewrite the violation-list section of format_text in src/skillsaw/formatters/text.py: sort by (file, line, severity), emit one relative-path header per file, then aligned columns of line:col, severity, rule id, message. Severity moves from section headers into a per-row column. Preserve the existing show_info/fail_level filtering and NO_COLOR handling.
  2. Dedup in two layers: within a file, collapse runs of identical rule+message into one row listing the line numbers; across files, fold verbatim-identical messages (the terminology case above) under the first occurrence as "same in N other files". This dedup design is the only contested surface and the main reason this is M rather than S — worth agreeing on the exact presentation before writing code.
  3. Add --statistics to the lint subparser in src/skillsaw/cli/_parser.py and wire it through src/skillsaw/cli/_lint.py: ruff-style per-rule violation counts, sorted descending. Text format only.
  4. Leave the Scanned: / Summary: / Rule docs sections and the grade block untouched; leave JSON/SARIF/HTML formatters unchanged.

Gotchas found during verification:

  • Breakage risk is low. Existing tests assert loosely on strings like Summary:, Errors:, and All checks passed — the Summary: block retains those labels, so most pass unchanged; there are no golden snapshots of text output. Tests that assert on the old per-severity section layout in tests/test_formatters.py will need updating.
  • action.yml never parses text violation lines, so CI consumers can't break on the layout change. It does have a pre-existing, unrelated bug worth its own issue: the lint step writes the report with --format text --output "$REPORT_FILE" and then json.loads it for the errors/warnings outputs — that parse always fails and is silently masked by 2>/dev/null || echo "0".
  • PR Close the lint-to-fix loop: fixable markers and fix hints in lint output #409 modifies the exact row this rewrites: it adds [*]/[?] fixability markers inside fmt_violation. The new grouped row format must carry those markers as a column; plan to land after Close the lint-to-fix loop: fixable markers and fix hints in lint output #409 (or rebase onto it) rather than racing it.

Files to touch:

  • src/skillsaw/formatters/text.py
  • src/skillsaw/formatters/__init__.py
  • src/skillsaw/cli/_parser.py
  • src/skillsaw/cli/_lint.py
  • tests/test_formatters.py
  • tests/test_integration.py
  • docs/cli.md
  • README.md

Scope

Effort: M — a focused rewrite of one formatter function plus one new flag; a few days including the dedup design discussion and test updates, not weeks.

Out of scope:

  • Any change to JSON/SARIF/HTML output or the Scanned:/Summary:/grade sections.
  • TTY detection, --color, or hyperlink work (separate idea, separate issue).
  • Fixing the action.yml summary-parsing bug noted above (separate issue; this change neither fixes nor worsens it).

Interactions with open PRs:


Surfaced and adversarially verified by a multi-agent codebase exploration. 🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    fable-5PR's/Issues filed by Fable

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions