fix(cli): one bad file costs one finding, not the whole Vale run - #315
Conversation
|
Verified independently before review, since this is the largest change in the batch. The discriminator holds for how the CLI actually invokes it. The absolute-vs-relative Worth noting the failure direction too, because it is the right one: if the discriminator ever misjudged, the existence check and the Mutation re-run independently: forcing the narrowing to never apply fails exactly the three tests meant to catch it, and reverting returns all 31 to green. One thing left undone that I agree with: Option 3 (making the payload unambiguous when — AI Coding Agent |
|
@claude /review |
|
Claude finished @thecodedrift's task in 6m 20s —— View job Claude finished @thecodedrift's task —— Todo
Review mode: incremental — read 0 prior review thread(s) before reviewing. SummaryThe core fix (retry-and-exclude in One likely correctness gap and two small cleanups, posted inline:
Nothing else stood out — the discriminator's absolute-vs-relative reasoning, the bounded-retry argument, and the |
…use formatter Addresses review feedback on #315: - targetFileParseError's `.taskless/`-prefix carve-out reintroduced the exact #300 bug on a path the carve-out itself was blind to: verifyValeRule points runVale explicitly at `.taskless/rules/vale/<ruleId>/rule-tests`, which is an explicit path, not a whole-project walk, so the `.taskless/**` glob exclusion never applies there and Vale really does walk into it. A malformed fixture reported a relative Path starting with `.taskless/rules/vale/...`, which the carve-out misread as "not a target," so the retry never fired and verifyValeRule failed the whole rule instead of excluding the one bad fixture. isAbsolute alone is the correct, and now the only, discriminator — a rule/style path reached through StylesPath is always absolute (pinned in vale-vendor-contract.test.ts), so any relative Path is by construction one of the run's own targets, .taskless/ or not. - The non-zero-exit branch in spawnVale parsed stderr twice: once by hand to populate configError, once again inside describeValeStderr. Extracted parseValeConfigError so both call sites share one parse. - The "Vale rejected the configuration" branch (reached via a zero-exit stdout payload, defensive/non-live per its own comment) built its message by hand instead of reusing formatValeConfigError, so it skipped that helper's multi-line cleanup and code-dedup logic. Added a regression test in vale-verify.test.ts exercising verifyValeRule with a malformed fail-fixture, mutation-checked by reinstating the removed carve-out and confirming it fails with the pre-fix symptom ("expected a verification, got Vale failed").
Addressed all three findings in 94ee60e:
|
A single markdown file with unparseable YAML front matter aborted the entire Vale invocation before any result was written, so `check` reported `results: []` for the whole run regardless of how many other files had findings — indistinguishable from a genuinely clean pass. runVale now retries: when Vale's own config-error payload attributes a failure to one of the run's target files (as opposed to a rule config it loaded through StylesPath, which always reports an absolute path), that file is excluded and reported as a per-file finding (ruleId: "vale-parse-error", severity: "error"), and the run continues over everything else. A failure that cannot be attributed to a single target file — a malformed rule, a timeout, a crash — still fails the run exactly as before. No YAML parser was added: Vale's own error object already names the file and the reason it could not be parsed, so the fix reuses that rather than re-deriving it with a second parser.
The #300 fix rests on a fact about VALE, not about this repository: a config error names a bad target file by its relative path as passed, and a bad rule file by an absolute path, because that one reaches Vale through StylesPath. `targetFileParseError` uses exactly that distinction to tell "one unreadable target, exclude it and retry" from "our own rule config is broken, stop". Nothing pinned it. The behaviour tests in `vale-run.test.ts` do run against the real binary and would fail if Vale changed, so this is not a coverage gap so much as a legibility one: they would report "expected ok to be failed" and leave someone to work backwards to the cause. This says which vendor assumption broke. It belongs in the vendor contract for a second reason. That file is what the upgrade procedure re-probes on every Vale bump, which is the moment this answer can change, and a bump is exactly when nobody is thinking about #300. Verified both directions against the pinned binary, and confirmed the assertion is real by inverting it and watching it fail. The failure direction stays safe either way: an unrecognised target error stops the run rather than excluding a rule config and continuing, so a change here degrades #300 back to its old behaviour rather than silently checking nothing. Refs #300
…use formatter Addresses review feedback on #315: - targetFileParseError's `.taskless/`-prefix carve-out reintroduced the exact #300 bug on a path the carve-out itself was blind to: verifyValeRule points runVale explicitly at `.taskless/rules/vale/<ruleId>/rule-tests`, which is an explicit path, not a whole-project walk, so the `.taskless/**` glob exclusion never applies there and Vale really does walk into it. A malformed fixture reported a relative Path starting with `.taskless/rules/vale/...`, which the carve-out misread as "not a target," so the retry never fired and verifyValeRule failed the whole rule instead of excluding the one bad fixture. isAbsolute alone is the correct, and now the only, discriminator — a rule/style path reached through StylesPath is always absolute (pinned in vale-vendor-contract.test.ts), so any relative Path is by construction one of the run's own targets, .taskless/ or not. - The non-zero-exit branch in spawnVale parsed stderr twice: once by hand to populate configError, once again inside describeValeStderr. Extracted parseValeConfigError so both call sites share one parse. - The "Vale rejected the configuration" branch (reached via a zero-exit stdout payload, defensive/non-live per its own comment) built its message by hand instead of reusing formatValeConfigError, so it skipped that helper's multi-line cleanup and code-dedup logic. Added a regression test in vale-verify.test.ts exercising verifyValeRule with a malformed fail-fixture, mutation-checked by reinstating the removed carve-out and confirming it fails with the pre-fix symptom ("expected a verification, got Vale failed").
94ee60e to
6a682d3
Compare
Summary
A single markdown file with unparseable YAML front matter aborted the entire Vale invocation before any result was written, so
checkreportedresults: []for the whole run regardless of how many other files had findings — indistinguishable from a genuinely clean pass.runValenow retries: when Vale's own config-error payload attributes a failure to one of the run's target files — as opposed to a rule config it loaded throughStylesPath, which always reports an absolute path — that file is excluded from the retry and reported as a per-file finding (ruleId: "vale-parse-error",severity: "error"), and the run continues over everything else. A failure that cannot be attributed to a single target file (a malformed rule, a timeout, a crash) still fails the run exactly as before.No YAML parser was added. Vale's own error object already names the offending file and why it could not be parsed; the fix reuses that rather than re-deriving the same fact with a second parser (see
STYLEGUIDE-CODE.md's "Verify Build Output In The Build, Not By Parsing It").Before:
{"success":false,"results":[],"failures":["Vale exited 2: E201: yaml: mapping values are not allowed in this context in content/blog/zzz-probe.md"]}After:
{"success":false,"results":[ {"source":"vale","ruleId":"vale-parse-error","severity":"error","file":"content/blog/zzz-probe.md","message":"Vale could not check this file: E201: yaml: mapping values are not allowed in this context", ...}, {"source":"vale","ruleId":"no-simply","severity":"warning","file":"content/blog/good-1.md", ...}, {"source":"vale","ruleId":"no-simply","severity":"warning","file":"content/blog/good-2.md", ...} ]}Test plan
packages/cli/test/vale-run.test.ts— unit coverage against the real Vale binary: one bad file among good ones, two bad files, the "empty results vs unreadable run" distinction the issue is about, and a regression check that a genuine rule-config error still blocks (not misattributed to a target file).packages/cli/test/mixed-engine-check.test.ts— end-to-end test spawning the built CLI over a scaffolded project reproducing the issue's exact repro.undefined), confirmed the new tests fail with the pre-fix symptom, reverted back, confirmed green.pnpm typecheck,pnpm test(1350 tests),pnpm lint(includespnpm cli checkagainst the built CLI) all pass.Fixes #300