feat!: promote performance rules to their own opt-in check section#41
Merged
Conversation
The performance heuristics (N+1 queries, alloc-heavy loops, sync I/O in request paths, unbounded concurrency) move out of the quality section into a new performance section with performance.* rule ids, enabled via checks.performance (off by default) and tuned via performance_rules. The Go sync-io/goroutine rules, previously ungated, now honor the detect_sync_io_in_handlers / detect_unbounded_concurrency toggles like their Python/TS counterparts. There is deliberately no runtime aliasing of the old ids: quality no longer emits them, and enabling the new section is the moment to migrate waivers/baselines. codeguard doctor now flags waivers that reference a retired quality.* performance id with the replacement id to use. The repo's own config enables the section and migrates its waiver. BREAKING CHANGE: the performance rules' ids renamed from quality.* to performance.* (quality.n-plus-one-query -> performance.n-plus-one-query, quality.go.alloc-in-loop -> performance.go.alloc-in-loop, quality.sync-io-in-request-path -> performance.sync-io-in-request-path, quality.unbounded-goroutines-in-loop -> performance.unbounded-goroutines-in-loop, plus the typescript/javascript mirrors and quality.python.sync-io-in-async). Their detect_* toggles moved from quality_rules to performance_rules, and the section is opt-in: set checks.performance: true to keep running these rules, and update any waivers or baselines that reference the old ids. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Configs written before the performance section exist have no checks.performance key. Text-format scan output now appends a one-line note pointing at the new section for those configs. The toggle is now a *bool so an explicit performance: false is distinguishable from an absent key and silences the note permanently; generated configs write the key explicitly, so only pre-existing configs see it. Machine-readable output formats (json/sarif/github) never include the note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… rules Eleven new rules across the performance section: - performance.regex-compile-in-loop (Go/Python/TS/JS): literal patterns compiled inside loop bodies; variable patterns are exempt because they usually differ per iteration - performance.go.defer-in-loop: defers accumulating in loops, scoped to the enclosing function so defer wg.Done() in loop-launched goroutines is not flagged - performance.go.sleep-in-loop: polling sleeps that want a ticker, channel signal, or backoff - performance.typescript/javascript.await-in-loop: sequential awaits that could batch via Promise.all; for-await streams and limiter-using files are exempt - performance.string-concat-in-loop (Python/TS/JS): += growth of string-initialized accumulators, under detect_alloc_in_loop - performance.python.unbounded-concurrency: asyncio task creation in loops without a Semaphore/TaskGroup/limiter in the file - performance.go.timer-leak-in-loop: time.After allocating uncollected timers per iteration - performance.typescript/javascript.timer-listener-leak: setInterval with no clearInterval in the file, and listeners added in loops with no removeEventListener/AbortSignal - performance.unbounded-read (Go/Python): io.ReadAll in handlers/loops (exempt when already bounded by LimitReader/MaxBytesReader) and unsized read()/readlines() in Python loops Each rule family has a performance_rules toggle (all default on), a catalog entry with fix template, tests including precision negatives, and docs coverage in docs/checks.md. Dogfooding the rules on this repo found and shaped two precision fixes (literal-only regex patterns, function-scoped defer) and added two reasoned waivers. Also adds a repository CLAUDE.md with build/test conventions and pointers to the .claude/knowledge context files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add performance.complexity-regression (toggle detect_complexity_regression, default on): in diff scans, compare each changed Go function's maximum loop-nesting depth against the same function at the diff base ref and warn when the depth increased, naming the function and both depths. Functions absent from the base revision (new or renamed) are skipped, and full scans are unaffected since the rule only activates in diff mode, mirroring the quality.coverage-delta precedent. Base content is read through the hardened ReadBaseFile git path; findings anchor to a changed line inside the function so they survive diff-scope filtering. Go-only in this first version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…React, and Express
Adds four framework-aware performance rules behind a single
performance_rules.detect_framework_patterns toggle (on by default within
the opt-in performance section). Every rule is gated on file-level
framework evidence (imports or obvious idioms) so non-framework code
never matches:
- performance.python.django-nplusone-relation: relation traversal
(item.relation.attr / item.relation_set.) on a queryset-loop variable
in a Django file with no select_related/prefetch_related anywhere;
chains whose final segment is called are exempt as scalar methods.
- performance.python.orm-query-in-loop: Django .objects.get/.filter and
SQLAlchemy session.get inside loop bodies; disjoint from the generic
n-plus-one pattern (loop headers and generically-matched lines skip),
and session.get requires a SQLAlchemy import so requests.Session
loops never match.
- performance.{typescript,javascript}.react-expensive-render: 2+ chained
.sort/.filter/.map calls, new Array(, or JSON.parse( in a React
component/custom-hook body; useMemo/useCallback/useEffect wrappers are
exempt via paren-depth region tracking.
- performance.{typescript,javascript}.express-sync-middleware: CPU-heavy
sync shortlist (bcrypt hashSync/compareSync, pbkdf2Sync/scryptSync,
zlib *Sync, execSync) inside app.use/router.use regions; takes
precedence over the generic sync-io-in-handler finding so one line
never reports twice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add performance.ai.semantic-perf, an opt-in LLM-assisted review of changed functions for concerns static rules cannot judge: repeated expensive calls that want caching/memoization, algorithmic complexity out of line with plausible input sizes, and obviously redundant work across the change. - new perf-focus prompt template mirroring the contract-drift one, with explicit non-goals (no micro-optimizations, no style, diff evidence only) - findings land in the Performance section: the performance package runs its own semantic pass, gated on checks.performance plus the same AI-runtime / CODEGUARD_SEMANTIC_CHECKS / semantic-command guards quality uses, and surfaces runtime breakage as performance.ai.semantic-runtime (fail) - one semantic request carries all lenses: quality and performance build byte-identical options via the new checks/semanticreview package and demultiplex the shared (cached) response by rule-id prefix; an in-process single-flight in ai/semantic prevents a duplicate LLM call when the two sections race on a cold verdict cache - with checks.performance disabled the request and output are byte-identical to before: the perf lens only rides along when the section is enabled - catalog + fix templates for both new rules; docs subsection under Performance incl. a natural-language custom-rule perf policy example Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two measurement-based gates join the performance section: - performance.budget (performance_rules.budgets): byte budgets over real artifacts — file/glob-total sizes (kind file-size) and bundler stats totals or per-asset sizes from esbuild metafiles / webpack stats JSON (kind bundle-stats). Warn by default, per-entry level: fail. A missing artifact is always a warn diagnostic, never a hard error. Budget paths are contained within the target (validation rejects absolute/..-paths; the check re-verifies after symlink resolution, fail closed) and stats reads are size-capped. - performance.benchmark-regression (performance_rules.benchmarks, off by default — it executes the repo's test code): runs go test -run=^$ -bench=. -benchmem via a govulncheck-style dedicated runner (runner/benchregression) with contained timeout and bounded output, parses standard benchmark lines, and warns when ns/op regresses beyond max_regression_percent (default 20) against a stored baseline. First run seeds the baseline (default: sibling of cache.path, contained like the other artifact paths); existing entries are never overwritten. The go binary is codeguard's own fixed tool invocation — no config command override, keeping the added trust surface zero; package patterns are charset-validated at config load and again before exec. Full scans require explicit benchmark packages; diff scans default to the packages containing changed .go files. Parser/comparator/baseline are pure functions tested without running benchmarks; one <2s integration test drives a real trivial benchmark module. Dogfood: doc size budgets in .codeguard/codeguard.yaml (always-resolvable artifacts — dist/ is not built in CI). pprof profile ingestion/fusion is documented as future work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… N+1 detection Register the python grammar in the production tree provider (scriptGrammar + ScriptLanguageForPath) and extend the grammar_subset release tag set with grammar_subset_python in the Makefile and goreleaser so tagged binaries only pay ~85 KB for the blob. scriptGrammar now probes the grammar registry before touching an accessor: under a grammar_subset build a missing grammar panics inside gotreesitter, which previously killed the whole section via safeRun instead of falling back. performance.n-plus-one-query for Python now prefers a tree query (call expressions with query-shaped callees inside for/while statements) modeled on the security_typescript_tree.go fallback pattern: same rule ID, level, and message as the regex path, confidence high, regex scan kept verbatim for parsers.treesitter off, missing grammar, or parse refusal. Query-shaped text inside comments and string literals no longer fires on the tree path. Also include cfg.Parsers in the performance section's cache-config family fingerprint: performance findings can now come from the tree path, so toggling parsers.treesitter must invalidate cached per-file findings. Differential tests pin both engines on one fixture: both find the genuine cursor.execute/requests.get calls in loops, and only the regex path keeps its pinned comment/string-literal false positives. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and report flag Mirror the quality section's slop_score pattern: when the performance section runs and has findings, each target publishes a performance_score artifact via env.PutArtifact. The score is a weighted finding count by rule family — N+1 query 5, blocking I/O 4, unbounded concurrency 4, memory pressure 3, repeated loop work 2, allocation churn 1 — scaled by 10 and capped at 100, with a per-rule component breakdown. The trend persists next to the scan cache (<cache>.perf-history.<ext>) following the slop-history pattern (runner/support/perf_score_history.go); later scans annotate the artifact with previous_score and delta. Persistence is gated by performance_rules.score_history (nil = enabled) and capped by performance_rules.score_history_limit. codeguard report -perf-history prints the recorded trend, mirroring -slop-history. Document the artifact, weights, report flag, and the Python tree-sitter precision upgrade in docs/checks.md, and exclude the new history file from git and the repository self-scan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alxxjohn
added a commit
that referenced
this pull request
Jul 16, 2026
… rules (#44) ## Summary Stacked on #41. Three tracks, built by parallel agents and integrated sequentially: codeguard's own scan gets measurably faster, the `repo_legibility` score becomes an enforceable and trended **AI-readiness gate**, and four new rules keep a repo's docs truthful for both AI agents and humans. ## 1. Scan performance (behavior-preserving, measured) | | before | after | |---|---|---| | Cold scan (this repo) | ~0.69 s | **~0.49 s** (−30%) | | Warm scan | ~0.60 s | **~0.46 s** (−24%) | - Quality's AI checks previously bypassed the shared scan corpus — 4–5 redundant full-corpus reads per scan (syscalls were 41% of warm CPU). New `ListTargetFiles`/`ReadTargetFile` corpus hooks route them all through one cached read; the redundant mode-0 re-parse now reuses the shared ParseComments AST. - Clone detector: tokens hashed once at tokenize time + O(1) rolling window hash, zero per-window allocations (was ~226 MB churn/scan). Hash is not behavior-bearing — every bucket pairing is still verified token-by-token. - One-pass dominant-style profile (test framework + error style + naming) replaces three corpus sweeps; allocation-free `CountLines`. - **Proof of identical behavior**: old vs new binaries produce byte-identical findings JSON on the same tree; a differential clone test captures the OLD algorithm's output verbatim and asserts the new one reproduces it. One semantic trap was found and neutralized: a `fn.Doc` directive exemption that never fired under the old parse mode would have silently activated — removed with a NOTE. ## 2. Enforceable AI-readiness score `repo_legibility` (0–100, explainable components) previously could not gate a build or show a trend. Now: - `context_rules.legibility_warn_threshold` / `legibility_fail_threshold` — fires `context.legibility-threshold` when the score falls **below** the floor (legibility is good-high; semantics documented), message carries the component breakdown. - History at `<cache>.legibility-history.<ext>` with previous_score/delta, plus `codeguard report -legibility-history`. - **Calibration fixes**: an empty CLAUDE.md no longer scores the full 25 agent-docs points (substance-gated + drift-penalized); context-economy ramps linearly to zero at 25% oversized (was a cliff at 10%); doc-accuracy penalty is proportional (broken/total) instead of saturating at 5 refs; conventional basenames (`index.ts`, `__init__.py`, `mod.rs`, `types.go`, …) no longer count as ambiguous — configurable via `ambiguous_symbol_ignore`. - Dogfooded: this repo runs with `legibility_warn_threshold: 85` as a live gate. ## 3. Doc-truth rules (Agent Context) | Rule | What it catches | |---|---| | `context.undocumented-commands` | high-signal Makefile targets / npm scripts (build, test, lint, fmt, …) that no agent doc or README mentions | | `context.oversized-agent-doc` | agent docs larger than `max_agent_doc_lines` (600) — they consume the context budget they exist to save | | `context.doc-link-rot` | markdown links in agent docs/README pointing at nonexistent files (relative + repo-absolute; no network I/O) | | README drift parity | `context.readme-drift` now gets the same full prose extraction as agent docs, not just shell fences | **Dogfooding found 17 real defects in our own docs** — README links baked to absolute `/Users/...` machine paths, references to nonexistent config filenames, and `make fmt` documented nowhere. All fixed, none waived. ## Verification 774 tests / 9 packages, go vet, gofmt, golangci-lint 0 issues, cache-cleared self-scan 0 fail with the legibility gate active. Docs fully updated: Agent Context section rewritten (new rules, recalibrated formulas, threshold/history semantics), architecture doc gained a corpus paragraph, and three new gotchas captured in .claude/knowledge/. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
alxxjohn
added a commit
that referenced
this pull request
Jul 16, 2026
🤖 I have created a release *beep* *boop* --- ## [1.0.0](v0.8.3...v1.0.0) (2026-07-16) ### ⚠ BREAKING CHANGES * promote performance rules to their own opt-in check section ([#41](#41)) * the performance rules' ids renamed from quality.* to performance.* (quality.n-plus-one-query -> performance.n-plus-one-query, quality.go.alloc-in-loop -> performance.go.alloc-in-loop, quality.sync-io-in-request-path -> performance.sync-io-in-request-path, quality.unbounded-goroutines-in-loop -> performance.unbounded-goroutines-in-loop, plus the typescript/javascript mirrors and quality.python.sync-io-in-async). Their detect_* toggles moved from quality_rules to performance_rules, and the section is opt-in: set checks.performance: true to keep running these rules, and update any waivers or baselines that reference the old ids. ### Features * 30% faster scans, enforceable AI-readiness score, and doc-truth rules ([#44](#44)) ([718dd08](718dd08)) * **context:** add AI-and-human-readiness rules and broaden README drift ([6caac9a](6caac9a)) * **context:** AI-readiness gate, doc-truth rules, and 30% faster scans ([1f4f130](1f4f130)) * **context:** AI-readiness gate, doc-truth rules, and 30% faster scans (re-land [#44](#44)) ([#46](#46)) ([75b3f02](75b3f02)) * **context:** enforceable legibility threshold and recalibrated score components ([604dd52](604dd52)) * **context:** persist repo_legibility score history with report flag ([e54f643](e54f643)) * **parsers:** wire the Python tree-sitter grammar and upgrade Python N+1 detection ([07a0b24](07a0b24)) * **performance:** add AI-assisted semantic performance lens ([eabd7fe](eabd7fe)) * **performance:** add diff-only loop-nesting complexity regression rule ([2e8bcef](2e8bcef)) * **performance:** add framework-aware rules for Django, SQLAlchemy, React, and Express ([b49ce7b](b49ce7b)) * **performance:** add loop-hygiene, concurrency, and memory-pressure rules ([54bfca9](54bfca9)) * **performance:** add measured budgets and benchmark regression gates ([2c5e4ba](2c5e4ba)) * **performance:** publish a performance_score artifact with history and report flag ([5c5a738](5c5a738)) * promote performance rules to their own opt-in check section ([ede8b37](ede8b37)) * promote performance rules to their own opt-in check section ([#41](#41)) ([81a575b](81a575b)) * suggest enabling the performance section in scan output ([81d9d46](81d9d46)) ### Performance Improvements * **quality:** hash clone tokens once and roll the window hash ([bbf5b69](bbf5b69)) * **quality:** route AI check reads through the shared scan corpus ([b271cfb](b271cfb)) * **runner:** count lines by scanning bytes instead of allocating ([17a4593](17a4593)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The complete 0.9.0 performance-section story, in three phases: (1) promote the performance rules to their own opt-in section, (2) upgrade-hint UX, (3) grow the section from 9 to 20 static rules, then (4) the advanced roadmap — change intelligence, framework awareness, measurement gates, an AI lens, and precision infrastructure — built by five parallel agents and integrated sequentially.
1. Performance becomes a first-class section (breaking, mitigated)
Rules moved out of quality with
quality.*→performance.*renames (full table in docs/checks.md migration note). Non-breaking in practice:checks.performanceis opt-in, so upgraders see strictly fewer findings; orphaned waivers go inert;codeguard doctornames the replacement id for any stale waiver. No runtime aliasing anywhere. Ships asfeat!:→ release-please cuts 0.9.0.2. Upgrade hint
Checks.Performanceis a*bool: configs predating the section (key absent) get a one-line note in text scan output; explicittrue/falsesilences it; machine formats never see it.3. Twenty static rules
Loop hygiene (regex-compile, defer, sleep, sequential-await), allocation (alloc/concat/prealloc), blocking I/O (Go handlers, TS/JS
*Sync, Python async), unbounded concurrency (goroutines/promises/asyncio tasks), memory pressure (time.Afterleaks, interval/listener leaks, unbounded reads), N+1 queries — with dogfood-shaped precision (literal-only regex patterns, function-scoped defer, bounded-reader exemptions, limiter/cleanup hints).4. Advanced roadmap (this update)
performance.complexity-regression, diff scans only): compares each changed function's max loop-nesting depth against the base ref and warns on increases — catches existing code moved inside iteration, the most common way perf regressions ship. Go, via the hardened base-ref git path.detect_framework_patterns): Django relation N+1 (select_related/prefetch_relatedaware), Django/SQLAlchemy ORM point queries in loops, React expensive-render (useMemo-exempt), Express CPU-heavy sync middleware — all gated on file-level framework evidence so non-framework code never matches, with generic-rule dedupe so one line never reports twice.performance_rules.budgets(file-size + esbuild/webpack bundle-stats kinds; path-contained, validated, level-configurable) andperformance_rules.benchmarks(opt-ingo test -benchvs a persisted baseline via a govulncheck-style bounded runner; baselines never silently reset; zero new trust surface — no config command override). pprof fusion documented as future work.performance.ai.semantic-perf): rides in the SAME semantic request as the quality lenses (single-flight + verdict cache ⇒ no extra LLM calls), demultiplexed by rule-id prefix into the Performance section; byte-identical requests/output when the section is off.grammar_subset_pythontag (+83 KB binary when enabled; Makefile + goreleaser updated); Python N+1 now runs on the AST with regex fallback — comments/strings no longer false-positive (differential tests prove it). Plus aperformance_scoreartifact (weighted per-family, documented weights) with per-target history andcodeguard report -perf-history.Two real bugs found and fixed during this work: missing grammars panicked under subset builds instead of falling back (now probed and degraded gracefully), and the performance cache fingerprint omitted
cfg.Parsers(toggling tree-sitter could serve stale findings).Dogfooding
The repo scans itself with the section enabled: waivers for its bounded worker pools and test polling loops, real doc-size budgets under
performance_rules.budgets, and its own perf-score history. The self-scan passes (0 fail).Verification
733 tests across 9 packages,
go vet,gofmt,golangci-lint0 issues, cache-clearedmake codeguard-ciself-scan green. Every rule has a catalog entry, fix template, toggle, docs table row, and positive + negative precision tests.🤖 Generated with Claude Code