Skip to content

feat!: promote performance rules to their own opt-in check section#41

Merged
alxxjohn merged 15 commits into
mainfrom
feat/performance-category
Jul 16, 2026
Merged

feat!: promote performance rules to their own opt-in check section#41
alxxjohn merged 15 commits into
mainfrom
feat/performance-category

Conversation

@alxxjohn

@alxxjohn alxxjohn commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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.performance is opt-in, so upgraders see strictly fewer findings; orphaned waivers go inert; codeguard doctor names the replacement id for any stale waiver. No runtime aliasing anywhere. Ships as feat!: → release-please cuts 0.9.0.

2. Upgrade hint

Checks.Performance is a *bool: configs predating the section (key absent) get a one-line note in text scan output; explicit true/false silences 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.After leaks, 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)

  • Complexity regression (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.
  • Framework-aware rules (detect_framework_patterns): Django relation N+1 (select_related/prefetch_related aware), 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.
  • Measurement gates: performance_rules.budgets (file-size + esbuild/webpack bundle-stats kinds; path-contained, validated, level-configurable) and performance_rules.benchmarks (opt-in go test -bench vs 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.
  • AI semantic perf lens (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.
  • Precision infrastructure: Python tree-sitter grammar wired into the production treeprovider path behind a new grammar_subset_python tag (+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 a performance_score artifact (weighted per-family, documented weights) with per-target history and codeguard 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-lint 0 issues, cache-cleared make codeguard-ci self-scan green. Every rule has a catalog entry, fix template, toggle, docs table row, and positive + negative precision tests.

🤖 Generated with Claude Code

alxxjohn and others added 15 commits July 15, 2026 22:21
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
alxxjohn merged commit 81a575b into main Jul 16, 2026
14 checks passed
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).
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.

1 participant