check abandons every file's findings when a single markdown file is large enough to exceed the Vale timeout. It is the same shape #300 fixed for unparseable files, on a path #300 did not cover, and the size threshold is far lower than it looks because Vale is quadratic.
Found while investigating a flaky test (#262 follow-up, #320), not in the field. Nobody has hit it yet that we know of.
Vale is quadratic in the size of ONE file
Measured against the pinned binary (@taskless/vale-darwin-arm64@3.19.0-...), one existence rule, one file, prose repeated to size:
| size |
time |
linear would be |
| 40KB |
0.077s |
— |
| 80KB |
0.30s |
0.15s |
| 160KB |
0.91s |
0.31s |
| 320KB |
3.50s |
0.62s |
| 640KB |
14.3s |
1.23s |
| 1MB |
48.5s |
2.0s |
Each doubling costs roughly 4x. This is upstream Vale's behaviour, not ours.
It is worth stressing that this is per-FILE, not per-corpus. The same ~1MB of prose spread across 400 files takes 190ms, versus 48s in one file. Volume is fine; size is not.
What our timeout does with that
VALE_TIMEOUT_MS is 60_000 (packages/cli/src/rules/vale/run.ts:47), and a timeout returns blocking: true and returns immediately:
return { status: "timeout", blocking: true, message: attempt.message };
It is NOT excluded-and-retried the way an unparseable file now is. The retry loop added for #300 narrows the run on a config error naming a target file; a timeout has no such path.
So one file over roughly 1.1MB takes Vale past 60s, and the entire run is blocked. Every other file's findings are lost, which is precisely the complaint in #300: "a checker's silence stops meaning anything."
Why the threshold is lower in practice
The 1.1MB figure is for a single rule on one machine. Real projects have several rules, slower or contended hardware, and CI runners. The quadratic means the margin does not degrade gently: 640KB is already 14 seconds, so a project can sit well inside the timeout while being one long note away from falling off it, and the failure arrives as a whole-run block rather than as a slow file.
Markdown of that size is unusual but not exotic: a generated changelog, an exported vault note, a data table pasted into prose. #300 was found on a 328-file markdown vault.
Suggested direction
Not obvious, and worth deciding rather than patching:
- Treat a timeout like a parse error where possible. Vale's timeout gives us no path, so there is nothing to exclude on the retry. Running per-file to attribute it would undo the batching the current design relies on.
- Report partial results instead of abandoning them. If a timeout kills a batch, findings already parsed from stdout could still be returned alongside a blocking notice naming what was not checked. That keeps one slow file from costing every other file's findings, which is the actual harm.
- Reconsider whether a timeout must be blocking at all. It is blocking today so a silently disabled engine cannot read as a clean pass, which is the right instinct. A partial result plus an explicit "these files were not checked" notice may serve that better than discarding everything.
- A per-file size guard that reports an oversized file as its own finding, the way an unparseable one now is, rather than letting it consume the batch's whole budget.
Repro
- A project with one Vale rule.
- A single
.md file of ~1.2MB of prose.
npx @taskless/cli check . --json
- The run blocks on the timeout;
results carries nothing for any other file.
The measurements above are reproducible with the binary directly, which is the faster way to see the curve.
checkabandons every file's findings when a single markdown file is large enough to exceed the Vale timeout. It is the same shape #300 fixed for unparseable files, on a path #300 did not cover, and the size threshold is far lower than it looks because Vale is quadratic.Found while investigating a flaky test (#262 follow-up, #320), not in the field. Nobody has hit it yet that we know of.
Vale is quadratic in the size of ONE file
Measured against the pinned binary (
@taskless/vale-darwin-arm64@3.19.0-...), oneexistencerule, one file, prose repeated to size:Each doubling costs roughly 4x. This is upstream Vale's behaviour, not ours.
It is worth stressing that this is per-FILE, not per-corpus. The same ~1MB of prose spread across 400 files takes 190ms, versus 48s in one file. Volume is fine; size is not.
What our timeout does with that
VALE_TIMEOUT_MSis60_000(packages/cli/src/rules/vale/run.ts:47), and a timeout returnsblocking: trueand returns immediately:It is NOT excluded-and-retried the way an unparseable file now is. The retry loop added for #300 narrows the run on a config error naming a target file; a timeout has no such path.
So one file over roughly 1.1MB takes Vale past 60s, and the entire run is blocked. Every other file's findings are lost, which is precisely the complaint in #300: "a checker's silence stops meaning anything."
Why the threshold is lower in practice
The 1.1MB figure is for a single rule on one machine. Real projects have several rules, slower or contended hardware, and CI runners. The quadratic means the margin does not degrade gently: 640KB is already 14 seconds, so a project can sit well inside the timeout while being one long note away from falling off it, and the failure arrives as a whole-run block rather than as a slow file.
Markdown of that size is unusual but not exotic: a generated changelog, an exported vault note, a data table pasted into prose. #300 was found on a 328-file markdown vault.
Suggested direction
Not obvious, and worth deciding rather than patching:
Repro
.mdfile of ~1.2MB of prose.npx @taskless/cli check . --jsonresultscarries nothing for any other file.The measurements above are reproducible with the binary directly, which is the faster way to see the curve.