Skip to content

Commit 33f77ca

Browse files
committed
Tooling: pre-commit --fast lane for the check runner
- New `IsFast: true` field on `CheckDefinition`, mirrors `IsSlow` / `CIOnly` / `FreestyleIncompat`. - 28 checks marked `IsFast`: formatters, non-compiling static linters, Go vet/staticcheck/tests, API server typecheck+tests, html-validate, warn-only metrics. - `--fast` filters the run to that set. Named `--check` invocations bypass the filter (same escape hatch as `IsSlow` / `CIOnly`). - `--fast` is mutually exclusive with `--include-slow` / `--only-slow`: combining them errors out at flag-parse time. - `FilterFastChecks` short-circuits when the flag is off, so main() stays under the gocyclo threshold. - `AGENTS.md` gains a three-cadence guidance block: `--fast` mid-work (~7 s), full suite before every commit, `--include-slow` before milestone wrap-up. Lists what `--fast` does and doesn't cover. - Docs in `scripts/check/CLAUDE.md` and `docs/tooling/testing.md` updated to match.
1 parent 6e37ab0 commit 33f77ca

6 files changed

Lines changed: 110 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,31 @@ Always use the checker script for compilation, linting, formatting, and tests. I
103103
full list, or multiple `--check` flags.
104104
- All Rust/Svelte checks: `./scripts/check.sh --rust` or `--svelte`
105105
- All checks: `./scripts/check.sh`
106+
107+
### When to run what
108+
109+
Three cadences. Pick the one that matches where you are in the work, not the one closest to "done."
110+
111+
- **`./scripts/check.sh --fast` — every few file edits, on a self-imposed rhythm (~7 s).** Don't wait for "before
112+
commit"; that's too late, by then a regression is buried under follow-up edits. Run after a small natural unit of
113+
work: a function rewritten, a test added, a config touched. Catches roughly half the things the full suite catches,
114+
for ~5% of the wall time, so use it liberally. The lane is editorially curated, not derived from timings; mutually
115+
exclusive with `--include-slow` / `--only-slow`. Covers:
116+
- All formatters (`oxfmt`, `rustfmt`, `gofmt`) and most non-compiling static linters (`cfg-gate`, `log-error-macro`,
117+
`error-string-match`, `ipc-enum-camelcase`, `cargo-machete`, `knip`, `import-cycles`, `type-drift`, `stylelint`,
118+
`css-unused`, `a11y-contrast`, `a11y-coverage`, `e2e-linux-typecheck`).
119+
- Go: `go-vet`, `staticcheck`, `ineffassign`, `misspell`, `gocyclo`, `go-tests`.
120+
- API server: `typecheck`, `tests`.
121+
- Website: `html-validate` (self-skips when `dist/` is absent).
122+
- Warn-only metrics: `file-length`, `claude-md-reminder`, `changelog-links`.
123+
- **Does NOT cover**: `clippy`, Rust tests, `cargo-audit`, `cargo-deny`, `jscpd`, `bindings-fresh`, desktop ESLint /
124+
`svelte-check` / Svelte tests, website ESLint / typecheck / build / e2e, `docker-build`, or any E2E suite.
125+
- **`./scripts/check.sh` — before every commit.** The full default suite (everything not marked `IsSlow`). Catches what
126+
`--fast` skips: `clippy`, Rust tests, audit/deny, svelte-check, website build, etc. This is the contract that what
127+
you're committing won't break CI.
128+
- **`./scripts/check.sh --include-slow` — before wrapping a milestone, declaring a feature done, or pushing a branch
129+
you've been sitting on.** Adds the slow lane on top of the default suite: `desktop-e2e-linux`,
130+
`desktop-e2e-playwright`, `rust-tests-linux`, `eslint-typecheck`. Allow ~20 min; this is the gate before "I'm done."
106131
- **`oxfmt` must always run before you call a task done.** It's monorepo-wide (markdown, YAML, JSON, JS/TS across every
107132
app) and takes ~1 second, so there's no reason to skip it. It's registered under `AppOther`, which means `--rust` and
108133
`--svelte` do NOT include it. If you only ran those, CI will catch unformatted markdown / JSON / etc. that you missed.

docs/tooling/testing.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,5 @@ race).
150150

151151
The single entry point for all linters, formatters, type checkers, and test runners. Always use it instead of raw
152152
`cargo`, `pnpm vitest`, `eslint`, etc. Its output is concise and CI-aligned. Per-check: `--check <name>`. By language:
153-
`--rust` / `--svelte`. Slow checks (E2E, Docker): `--only-slow`. See AGENTS.md "Testing and checking".
153+
`--rust` / `--svelte`. Fast pre-commit lane (~7 s, curated): `--fast`. Slow checks (E2E, Docker): `--only-slow`. See
154+
AGENTS.md "Testing and checking" for the three-cadence guidance.

scripts/check/CLAUDE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ go run ./scripts/check --include-slow
2424
# Run only slow checks
2525
go run ./scripts/check --only-slow
2626

27+
# Run only the curated fast pre-commit lane (~10s)
28+
go run ./scripts/check --fast
29+
2730
# CI mode (no auto-fixing, stop on first failure)
2831
go run ./scripts/check --ci --fail-fast
2932

@@ -46,6 +49,7 @@ go run ./scripts/check --only-freestyle
4649
| `--verbose` | Show detailed output |
4750
| `--include-slow` | Include slow checks (excluded by default) |
4851
| `--only-slow` | Run only slow checks |
52+
| `--fast` | Run only the curated fast pre-commit check set |
4953
| `--only-freestyle` | Run freestyle-compatible checks on a VM (skip the rest) |
5054
| `--prefer-freestyle` | Run compat checks on VM + the rest locally in parallel |
5155
| `--fail-fast` | Stop on first failure |
@@ -70,6 +74,7 @@ go run ./scripts/check --only-freestyle
7074
-> selectChecks() # filter AllChecks by flags
7175
-> FilterSlowChecks() # drop IsSlow=true unless --include-slow or --check used
7276
-> FilterCIOnlyChecks() # drop CIOnly=true unless --ci or --check named it
77+
-> FilterFastChecks() # if --fast: keep IsFast=true (or named via --check)
7378
-> ensurePnpmDependencies() # pnpm install once at root (skipped for Rust-only runs)
7479
-> Runner.Run():
7580
goroutine pool (NumCPU semaphore)
@@ -115,6 +120,12 @@ counted as failed. Dependencies not in the selected run set are treated as satis
115120
`desktop-e2e-playwright`). Named `--check` invocations implicitly include slow checks
116121
(`includeSlow = len(checkNames) > 0`).
117122

123+
**Fast lane (`--fast`):** `IsFast: true` marks the curated pre-commit check set: ~28 checks that finish in roughly 10s
124+
on a warm cache, intended to run before every commit. It's an editorial pick, not a timing-derived list (see Key
125+
decisions below). Named `--check` invocations bypass the filter so `--fast --check svelte-check` still runs
126+
svelte-check. Mutually exclusive with `--include-slow` / `--only-slow` — combining them errors out, since the lanes are
127+
intentionally separate.
128+
118129
**CI-only checks:** `CIOnly: true` marks checks that run only in `--ci` mode (currently: `cargo-udeps`). They're
119130
silently dropped from local runs (no SKIPPED line) and are not pulled in by `--include-slow` or `--only-slow`. Escape
120131
hatch: an explicit `--check cargo-udeps` always runs, so you can verify locally before pushing.
@@ -161,6 +172,7 @@ DisplayName: "eslint", // shown in output
161172
App: AppDesktop,
162173
Tech: "🎨 Svelte",
163174
IsSlow: false,
175+
IsFast: false, // true = included in --fast (curated pre-commit lane)
164176
CIOnly: false, // true = run only in --ci mode (or explicit --check)
165177
FreestyleIncompat: true, // can NOT run on freestyle.sh VMs (Rust, Docker)
166178
DependsOn: []string{"desktop-svelte-prettier"},
@@ -305,6 +317,14 @@ Orthogonal to `IsSlow`: `--include-slow` and `--only-slow` do NOT pull in CI-onl
305317
ability to run "all slow checks locally without the CI-only ones"). Negative-sense default (`false` = runs locally)
306318
matches the other gating fields.
307319

320+
**Decision**: `IsFast` field on `CheckDefinition` and a curated `--fast` pre-commit lane. **Why**: A pre-commit run
321+
should finish in ~10s so it actually gets used. The list is editorially curated, not derived from CSV timings: warm
322+
average is what matters, but cold-cache outliers (`cargo-audit` spiking to ~3 min on advisory DB refresh) would silently
323+
make the lane unreliable on the first run of the day. Mirrors the `IsSlow` / `CIOnly` field pattern (negative-sense
324+
boolean default, same colocated style). Mutually exclusive with `--include-slow` / `--only-slow` to keep the semantics
325+
unambiguous: "give me the fast lane" and "give me the slow lane" can't both be true. Named `--check` invocations bypass
326+
the filter (same escape hatch as `IsSlow` and `CIOnly`).
327+
308328
**Decision**: Skip `pnpm install` when lockfile is unchanged. **Why**: `pnpm install` takes ~20s and pegs all CPUs even
309329
when deps haven't changed. A marker file (`node_modules/.pnpm-install-marker`) stores `pnpm-lock.yaml`'s mtime after
310330
each successful install. On the next run, if the mtime matches, install is skipped. The marker lives inside

scripts/check/checks/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ type CheckDefinition struct {
9090
App App
9191
Tech string
9292
IsSlow bool
93+
IsFast bool // true = included in --fast (pre-commit lane). Curated, not derived.
9394
CIOnly bool // true = run only when --ci is set (or when explicitly named via --check)
9495
FreestyleIncompat bool // true = can NOT run on freestyle.sh VMs (Rust compilation, Docker, etc.)
9596
// NeedsSmb declares that this check requires the smb-consumer Docker stack

scripts/check/checks/registry.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var AllChecks = []CheckDefinition{
1313
Tech: "📐 Format",
1414
FreestyleIncompat: true,
1515
DependsOn: nil,
16+
IsFast: true,
1617
Run: RunOxfmt,
1718
},
1819

@@ -25,6 +26,7 @@ var AllChecks = []CheckDefinition{
2526
Tech: "🦀 Rust",
2627
FreestyleIncompat: true,
2728
DependsOn: nil,
29+
IsFast: true,
2830
Run: RunRustfmt,
2931
},
3032
{
@@ -65,6 +67,7 @@ var AllChecks = []CheckDefinition{
6567
Tech: "🦀 Rust",
6668
FreestyleIncompat: true,
6769
DependsOn: nil,
70+
IsFast: true,
6871
Run: RunCargoMachete,
6972
},
7073
{
@@ -96,6 +99,7 @@ var AllChecks = []CheckDefinition{
9699
Tech: "🦀 Rust",
97100
FreestyleIncompat: true,
98101
DependsOn: nil,
102+
IsFast: true,
99103
Run: RunCfgGate,
100104
},
101105
{
@@ -106,6 +110,7 @@ var AllChecks = []CheckDefinition{
106110
Tech: "🦀 Rust",
107111
FreestyleIncompat: false,
108112
DependsOn: nil,
113+
IsFast: true,
109114
Run: RunLogErrorMacro,
110115
},
111116
{
@@ -116,6 +121,7 @@ var AllChecks = []CheckDefinition{
116121
Tech: "🦀 Rust",
117122
FreestyleIncompat: false,
118123
DependsOn: nil,
124+
IsFast: true,
119125
Run: RunErrorStringMatch,
120126
},
121127
{
@@ -136,6 +142,7 @@ var AllChecks = []CheckDefinition{
136142
Tech: "🦀 Rust",
137143
FreestyleIncompat: false,
138144
DependsOn: nil,
145+
IsFast: true,
139146
Run: RunIpcEnumCamelCase,
140147
},
141148
{
@@ -197,6 +204,7 @@ var AllChecks = []CheckDefinition{
197204
App: AppDesktop,
198205
Tech: "🎨 Svelte",
199206
DependsOn: []string{"oxfmt"},
207+
IsFast: true,
200208
Run: RunStylelint,
201209
},
202210
{
@@ -206,6 +214,7 @@ var AllChecks = []CheckDefinition{
206214
App: AppDesktop,
207215
Tech: "🎨 Svelte",
208216
DependsOn: []string{"desktop-svelte-stylelint"},
217+
IsFast: true,
209218
Run: RunCSSUnused,
210219
},
211220
{
@@ -215,6 +224,7 @@ var AllChecks = []CheckDefinition{
215224
App: AppDesktop,
216225
Tech: "🎨 Svelte",
217226
DependsOn: []string{"desktop-svelte-stylelint"},
227+
IsFast: true,
218228
Run: RunA11yContrast,
219229
},
220230
{
@@ -223,6 +233,7 @@ var AllChecks = []CheckDefinition{
223233
DisplayName: "a11y-coverage",
224234
App: AppDesktop,
225235
Tech: "🎨 Svelte",
236+
IsFast: true,
226237
Run: RunA11yCoverage,
227238
},
228239
{
@@ -241,6 +252,7 @@ var AllChecks = []CheckDefinition{
241252
App: AppDesktop,
242253
Tech: "🎨 Svelte",
243254
DependsOn: nil,
255+
IsFast: true,
244256
Run: RunImportCycles,
245257
},
246258
{
@@ -250,6 +262,7 @@ var AllChecks = []CheckDefinition{
250262
App: AppDesktop,
251263
Tech: "🎨 Svelte",
252264
DependsOn: nil,
265+
IsFast: true,
253266
Run: RunKnip,
254267
},
255268
{
@@ -259,6 +272,7 @@ var AllChecks = []CheckDefinition{
259272
App: AppDesktop,
260273
Tech: "🎨 Svelte",
261274
DependsOn: nil,
275+
IsFast: true,
262276
Run: RunTypeDrift,
263277
},
264278
{
@@ -277,6 +291,7 @@ var AllChecks = []CheckDefinition{
277291
App: AppDesktop,
278292
Tech: "🎨 Svelte",
279293
DependsOn: nil,
294+
IsFast: true,
280295
Run: RunDesktopE2ELinuxTypecheck,
281296
},
282297
{
@@ -344,6 +359,7 @@ var AllChecks = []CheckDefinition{
344359
App: AppWebsite,
345360
Tech: "🚀 Astro",
346361
DependsOn: []string{"website-build"},
362+
IsFast: true,
347363
Run: RunWebsiteHTMLValidate,
348364
},
349365
{
@@ -370,6 +386,7 @@ var AllChecks = []CheckDefinition{
370386
App: AppApiServer,
371387
Tech: "⸆⸉ TS",
372388
DependsOn: []string{"api-server-eslint"},
389+
IsFast: true,
373390
Run: RunApiServerTypecheck,
374391
},
375392
{
@@ -378,6 +395,7 @@ var AllChecks = []CheckDefinition{
378395
App: AppApiServer,
379396
Tech: "⸆⸉ TS",
380397
DependsOn: []string{"api-server-typecheck"},
398+
IsFast: true,
381399
Run: RunApiServerTests,
382400
},
383401

@@ -390,6 +408,7 @@ var AllChecks = []CheckDefinition{
390408
Tech: "🐹 Go",
391409
FreestyleIncompat: true,
392410
DependsOn: nil,
411+
IsFast: true,
393412
Run: RunGoFmt,
394413
},
395414
{
@@ -399,6 +418,7 @@ var AllChecks = []CheckDefinition{
399418
App: AppScripts,
400419
Tech: "🐹 Go",
401420
DependsOn: []string{"scripts-go-gofmt"},
421+
IsFast: true,
402422
Run: RunGoVet,
403423
},
404424
{
@@ -408,6 +428,7 @@ var AllChecks = []CheckDefinition{
408428
App: AppScripts,
409429
Tech: "🐹 Go",
410430
DependsOn: []string{"scripts-go-gofmt"},
431+
IsFast: true,
411432
Run: RunStaticcheck,
412433
},
413434
{
@@ -417,6 +438,7 @@ var AllChecks = []CheckDefinition{
417438
App: AppScripts,
418439
Tech: "🐹 Go",
419440
DependsOn: []string{"scripts-go-gofmt"},
441+
IsFast: true,
420442
Run: RunIneffassign,
421443
},
422444
{
@@ -426,6 +448,7 @@ var AllChecks = []CheckDefinition{
426448
App: AppScripts,
427449
Tech: "🐹 Go",
428450
DependsOn: nil,
451+
IsFast: true,
429452
Run: RunMisspell,
430453
},
431454
{
@@ -435,6 +458,7 @@ var AllChecks = []CheckDefinition{
435458
App: AppScripts,
436459
Tech: "🐹 Go",
437460
DependsOn: []string{"scripts-go-gofmt"},
461+
IsFast: true,
438462
Run: RunGocyclo,
439463
},
440464
{
@@ -462,6 +486,7 @@ var AllChecks = []CheckDefinition{
462486
App: AppScripts,
463487
Tech: "🐹 Go",
464488
DependsOn: []string{"scripts-go-vet"},
489+
IsFast: true,
465490
Run: RunGoTests,
466491
},
467492

@@ -472,6 +497,7 @@ var AllChecks = []CheckDefinition{
472497
App: AppOther,
473498
Tech: "📏 Metrics",
474499
DependsOn: nil,
500+
IsFast: true,
475501
Run: RunFileLength,
476502
},
477503
{
@@ -480,6 +506,7 @@ var AllChecks = []CheckDefinition{
480506
App: AppOther,
481507
Tech: "📏 Metrics",
482508
DependsOn: nil,
509+
IsFast: true,
483510
Run: RunClaudeMdReminder,
484511
},
485512
{
@@ -489,6 +516,7 @@ var AllChecks = []CheckDefinition{
489516
App: AppOther,
490517
Tech: "🔗 Links",
491518
DependsOn: nil,
519+
IsFast: true,
492520
Run: RunChangelogCommitLinks,
493521
},
494522
}
@@ -592,6 +620,29 @@ func FilterSlowChecks(defs []CheckDefinition, includeSlow bool) []CheckDefinitio
592620
return result
593621
}
594622

623+
// FilterFastChecks keeps only checks marked IsFast (the curated pre-commit
624+
// lane) when `fast` is true; otherwise returns `defs` unchanged. Checks the
625+
// user explicitly named via --check bypass the filter, so
626+
// `--fast --check svelte-check` still runs svelte-check alongside the fast set.
627+
func FilterFastChecks(defs []CheckDefinition, fast bool, namedChecks []string) []CheckDefinition {
628+
if !fast {
629+
return defs
630+
}
631+
named := make(map[string]bool, len(namedChecks))
632+
for _, name := range namedChecks {
633+
if c := GetCheckByID(name); c != nil {
634+
named[c.ID] = true
635+
}
636+
}
637+
var result []CheckDefinition
638+
for _, def := range defs {
639+
if def.IsFast || named[def.ID] {
640+
result = append(result, def)
641+
}
642+
}
643+
return result
644+
}
645+
595646
// FilterCIOnlyChecks removes CI-only checks unless we're running in CI mode
596647
// or the user explicitly named them via --check. The named-check escape hatch
597648
// lets developers verify a CI-only check locally before pushing.

0 commit comments

Comments
 (0)