Skip to content

fix(cli): correct raw-scope Vale line numbers - #314

Merged
thecodedrift merged 2 commits into
fix/init-json-envelopefrom
fix/vale-finding-line-numbers
Sep 8, 2026
Merged

fix(cli): correct raw-scope Vale line numbers#314
thecodedrift merged 2 commits into
fix/init-json-envelopefrom
fix/vale-finding-line-numbers

Conversation

@thecodedrift

Copy link
Copy Markdown
Member

Summary

check --json reported a raw-scope Vale finding's line one line earlier than the flagged text (#297). raw patterns are conventionally anchored with a leading \n so they can require "start of line" against the unparsed document. That \n is part of Vale's reported match, and Vale attributes Line to the newline ending the previous line rather than to the line the flagged text is actually on.

toValeCheckResult now counts a match's leading newlines (via Match, already on the payload) and adds them back before the existing 1-based-to-0-based conversion.

The other half of #297 was not a bug

The issue also reported default-scope findings landing "one line early." That is CheckResult.range working as documented: it is 0-indexed for every source (ast-grep's native range passes straight through, format.ts adds 1 back for every source when it displays), and Vale itself reports the correct 1-based line for a default-scope match. The issue's table compared that 0-based JSON payload against a 1-based file line number.

Established by:

  • reading map.ts's existing comment and vale-map.test.ts's existing worked example, which already encodes Line: 3range.start.line: 2
  • confirming format.ts:12 adds + 1 back when it displays range.start.line
  • confirming check.ts emits results verbatim under --json with no re-conversion
  • running the real Vale binary against a fixture, which reported the true 1-based line for a default-scope match with no offset of its own

Measured

Repro with the built CLI on a fixture with front matter (doc lines 1-based: to be honest on line 7, raw target on line 9):

rule before (range.start.line, 0-based) after true (1-based)
default-scope 6 6 (unchanged) 7
raw-scope 7 8 9

Before the fix, raw was one line short of the 0-based value that matches the default-scope contract (6 vs 8 would be the base+2 shift the issue measured in 1-based terms across scopes; here shown as the 0-based JSON values the built CLI actually emits).

Tests

Added to packages/cli/test/vale-map.test.ts:

  • synthetic toValeCheckResult cases for 1 leading newline, 2 leading newlines, no leading newline (default-scope), and an embedded (non-leading) newline
  • a real-Vale-binary suite (toValeCheckResult against the real Vale binary) with a fixture that has YAML front matter, one default-scope rule, and one raw-scope rule, asserting range.start.line for both

Mutation-tested: reverting the leading-newline correction failed exactly the 3 tests meant to catch it (2 synthetic + the real-binary raw test) while both default-scope tests stayed green; substituting a naive "count all newlines" implementation was caught by the "does not count a newline appearing after the match's start" test and nothing else.

Verification

  • pnpm typecheck — pass
  • pnpm test — 1351/1351 pass
  • pnpm lint — pass (check:style reports "No issues found")
  • Reproduced the issue end-to-end with the built CLI (check doc.md --json) both before and after the fix, shown above

No changeset needed for the documentation half since no behavior changed there; one changeset added (patch, pre-1.0 per repo convention) for the raw-scope line fix.

Fixes #297

@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 6m 27s —— View job


Review complete

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json held no review threads, reviews, or resolved comments — only the @claude /review trigger comment — so this is the first review of this PR; everything below is assessed against the whole diff.

Todo list

  • Read .prior-review.json (no prior findings to classify)
  • Gather context (PR body, changed files)
  • Read the diff (gh pr diff) and full contents of changed files
  • Review packages/cli/src/rules/vale/map.ts for correctness
  • Review packages/cli/test/vale-map.test.ts for adequacy
  • Review changeset file
  • Post inline comments for issues found
  • Post final summary

Overall assessment

The core diagnosis and fix are sound: leadingNewlines counts a raw-scope match's leading \ns and adds them back before the existing 1-based→0-based conversion, which is the right root-cause fix (it generalizes to any number of leading newlines rather than hard-coding +1), and it's well covered by both synthetic unit tests and a real-Vale-binary regression suite for the primary (single-newline) case. The write-up correctly separates the real bug (raw-scope) from the non-bug (default-scope range being 0-indexed by design, confirmed by format.ts's +1 on display) — I re-derived the same conclusion independently from map.ts's docstring, toValeCheckResults' single call site, and the format.ts display path.

Two things worth a second look before merge, posted as inline comments:

  1. packages/cli/src/rules/vale/map.ts (line ~191) — the fix corrects line for a raw-scope leading-newline match but leaves startColumn/endColumn (from finding.Span) untouched. If Vale computes Span relative to the same (wrong) line it attributes to Line, a raw match anchored on \n right after a non-blank line could still report a wrong column, even though Line is now correct. The PR's fixture happens to have a blank line before the raw target, which may be masking this. I couldn't run the real binary in this sandbox to confirm either way (no node_modules/Vale binary installed), so this is flagged as a plausible gap for the author to check, not a confirmed bug.
  2. packages/cli/test/vale-map.test.ts (lines ~122–131) — the 2-leading-newline case is only verified synthetically, not against the real Vale binary the way the 1-leading-newline case is. It's cheap insurance to extend the real-binary suite with a \n\n-anchored raw rule to confirm Vale's line-attribution model actually generalizes the way the synthetic test assumes.

Everything else — import ordering/grouping (matches .conventions/STYLEGUIDE-CODE.md), the changeset's patch bump (correct for pre-1.0 per repo convention), test cleanup (afterEach + rmSync on temp workspaces, mirroring the existing pattern in vale-run.test.ts), and the docstring updates — looked correct and proportionate to the fix. No CLAUDE.md violations found in the diff.

Comment thread packages/cli/src/rules/vale/map.ts Outdated
Comment thread packages/cli/test/vale-map.test.ts
thecodedrift added a commit that referenced this pull request Sep 8, 2026
…et as the line

Review on #314 caught a real gap in the line fix: `startColumn`/
`endColumn` still came straight from `finding.Span` unchanged, but
`Span` is measured against the line Vale (wrongly) attributed a
leading-newline match to, not the corrected line the range now
reports. Confirmed against the real binary with a raw match preceded
by a non-blank line: Vale reports `Span: [89, 117]`, which is that
89-character... rather, that preceding line's length (88) plus one
through the whole match's length minus one — not a real column on any
line. The original fixture's raw targets both happened to follow a
*blank* line, whose zero length made the uncorrected column look
right by coincidence.

The character immediately after a `\n` is always column 1 of the next
line, regardless of the preceding line's length or how many leading
newlines the match opened with. So once a match has any leading
newlines, `rawScopeColumns` now reports column 0 as the start and the
match's length after stripping those newlines, minus one, as the end.

Also closes the second review item: the "two leading newlines"
line-advancement case was only covered synthetically. Added a
real-Vale-binary rule anchored on `\n\n` to the existing fixture, so
the newline *count*, not just its presence, is pinned against the
vendor's actual behavior for both line and column.

Mutation-tested: reverting `rawScopeColumns` to pass `Span` through
unchanged failed exactly the 4 tests written for it (2 synthetic + 2
real-binary); nudging the reset start column from 0 to 1 was caught by
the same 4; collapsing `leadingNewlines` to "1 if any, else 0" was
caught by the two-newline synthetic test and its real-binary
counterpart, and nothing else failed in any case.
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Claude finished @thecodedrift's task in 6m 27s"

Both flagged items were real, and both are fixed:

  1. Column correction. startColumn/endColumn came from finding.Span unchanged, and Span is measured against the line Vale (wrongly) attributed a leading-newline match to — not the corrected line. Confirmed against the real binary with a non-blank preceding line (Span: [89, 117], i.e. the 88-character predecessor's length + 1, through the whole match's length − 1 — not a real column on any line). The original fixture's blank-line predecessors made the bug invisible by coincidence. rawScopeColumns now resets the start column to 0 and computes the end column from the match's length after stripping its leading newlines, since the character right after a \n is always column 1 of the next line.

  2. Two-newline real-binary coverage. Added a \n\n-anchored rule to the existing real-Vale-binary fixture, so the newline count — not just its presence — is pinned against Vale's actual attribution behavior for both line and column, matching the pattern already used for the single-newline case.

New commit: correct raw-scope columns for the same leading-newline offset as the line — a8208d6.

Verified: pnpm typecheck, pnpm test (1356/1356), pnpm lint all pass. Mutation-tested each change (details in the inline replies) — every mutation was caught by exactly the tests written for it, no others.

…0-based contract

Vale's `raw` patterns are conventionally anchored with a leading `\n` so
they can require "start of line" against the unparsed document. That
`\n` is part of the reported match, and Vale attributes `Line` to the
newline ending the previous line rather than to the line the flagged
text is actually on, so a raw-scope finding always landed one line
early on top of the mapper's existing 1-based-to-0-based conversion.

`toValeCheckResult` now counts a match's leading newlines and adds
them back before converting to the 0-indexed `CheckResult.range`.

The other half of #297 — default-scope findings reading "one line
early" — is not a bug: `CheckResult.range` is 0-indexed by design for
every source (ast-grep's native range passes straight through, and
`format.ts` adds 1 back for every source when it displays). Vale
itself reports the correct 1-based line for a default-scope match;
the issue's table compared that 0-based JSON output against a 1-based
file line number. Confirmed by reading map.ts's existing comment and
tests, and by running the real Vale binary against a fixture with
front matter.

Added tests pinning both: a default-scope rule on a fixture with
front matter as a regression guard for the working-as-designed half,
and both synthetic and real-Vale-binary raw-scope tests for the fix.
Mutation-tested by reverting the leading-newline correction and by
substituting a broken newline-counting implementation; both mutations
were caught by the new tests and only those tests.
…et as the line

Review on #314 caught a real gap in the line fix: `startColumn`/
`endColumn` still came straight from `finding.Span` unchanged, but
`Span` is measured against the line Vale (wrongly) attributed a
leading-newline match to, not the corrected line the range now
reports. Confirmed against the real binary with a raw match preceded
by a non-blank line: Vale reports `Span: [89, 117]`, which is that
89-character... rather, that preceding line's length (88) plus one
through the whole match's length minus one — not a real column on any
line. The original fixture's raw targets both happened to follow a
*blank* line, whose zero length made the uncorrected column look
right by coincidence.

The character immediately after a `\n` is always column 1 of the next
line, regardless of the preceding line's length or how many leading
newlines the match opened with. So once a match has any leading
newlines, `rawScopeColumns` now reports column 0 as the start and the
match's length after stripping those newlines, minus one, as the end.

Also closes the second review item: the "two leading newlines"
line-advancement case was only covered synthetically. Added a
real-Vale-binary rule anchored on `\n\n` to the existing fixture, so
the newline *count*, not just its presence, is pinned against the
vendor's actual behavior for both line and column.

Mutation-tested: reverting `rawScopeColumns` to pass `Span` through
unchanged failed exactly the 4 tests written for it (2 synthetic + 2
real-binary); nudging the reset start column from 0 to 1 was caught by
the same 4; collapsing `leadingNewlines` to "1 if any, else 0" was
caught by the two-newline synthetic test and its real-binary
counterpart, and nothing else failed in any case.
@thecodedrift
thecodedrift force-pushed the fix/vale-finding-line-numbers branch from a8208d6 to 9ca3302 Compare September 8, 2026 16:51
@thecodedrift
thecodedrift changed the base branch from main to fix/init-json-envelope September 8, 2026 16:51
@thecodedrift
thecodedrift merged commit beea788 into main Sep 8, 2026
8 checks passed
@thecodedrift
thecodedrift deleted the fix/vale-finding-line-numbers branch September 8, 2026 16:55
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.

check --json reports Vale findings 1 line early, or 2 lines early for raw-scope rules

1 participant