You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## 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)
Copy file name to clipboardExpand all lines: .claude/knowledge/architecture-boundaries.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,3 +30,6 @@ Key architectural decisions, service boundaries, data flow, integration points,
30
30
-**The usage menu makes one outbound call (opt-out).** Running `codeguard` with no args or `help` renders a "What's New" banner (`internal/whatsnew` + `internal/cli/whatsnew.go`) that queries the GitHub releases API for the latest version. It is disabled when `CODEGUARD_NO_UPDATE_CHECK` is truthy OR `$CI` is non-empty (so CI/automated runs make no surprise call), caches the result for 24h under `os.UserCacheDir()/codeguard/update-check.json`, uses the SSRF-guarded `ai/safehttp.Client` with a 1.5s timeout, and fails silent (falls back to stale cache, then to version-only). Changelog bullets come from `CHANGELOG.md` embedded via a **module-root package** (`/changelog.go`, `package codeguard`, imported as `root "github.com/devr-tools/codeguard"`) — `go:embed` cannot reference `../CHANGELOG.md`, so the embed directive must live at the repo root, not in `internal/`.
31
31
-**Lint is enforced in CI.**`make lint` is `go vet`; `make lint-strict` / the CI `golangci-lint-action` step (now blocking — no `continue-on-error`) runs the 16-linter `.golangci.yml`. `golangci-lint run` must report 0 before pushing. The config excludes revive's documentation/naming style rules (`exported`, `package-comments`, `unused-parameter`) and excludes `tests/` from gosec/errcheck/prealloc/bodyclose (intentional fixtures). Cache/identity keys use sha256 (not sha1). Documenting the public `pkg/codeguard` API with doc comments is a known follow-up (revive `exported` is currently excluded rather than satisfied).
32
32
- **Adding a check section touches a fixed wiring list; two are silent-failure traps.** Beyond the obvious (check package with `Run(ctx, env) core.SectionResult`, a `sectionDef` in `runner/checks/registry.go`, a `CheckConfig` bool + rules struct in `core/config_types.go`/`config_rule_types.go`, catalog + fix-template maps merged in `rules/catalog.go`/`catalog_fix_templates.go`, defaults in `config/defaults*.go` + `example*.go`, docs): (1) `runner/support/cache_helpers.go` — add the section to `SectionConfigHashes` AND `sectionConfigFamily`, else the section's per-file cache keys fall back to the all-checks fingerprint (correct but invalidates on any config change); (2) top-level `CheckConfig` bools get NO defaulting in `applyCheckDefaults` — an omitted `checks.<section>` key means **off**, which is how the opt-in `performance` and `supply_chain` sections work. The performance section (v0.9.0) is the reference move: rules migrated from quality with renamed ids (`quality.*` → `performance.*`), no runtime aliasing, `rules.RetiredPerformanceRuleIDs()` powers a `codeguard doctor` waiver migration hint.
33
+
-**PERF: the quality AI checks read through the corpus via `checks/support.Context.ListTargetFiles`/`ReadTargetFile`** (wired in `runner/checks/checks.go`, backed by `runnersupport.ListTargetFiles`/`ReadTargetFile`; `checks/quality/quality_ai_corpus.go` holds the nil-safe fallbacks for unit-test contexts). GOTCHA when routing more reads through the corpus: `corpusRead` caps at 32 MiB (`readCappedFile`) while `os.ReadFile` does not — walk-enumerated files are already under the cap so behavior is identical, but fixed-filename reads that never went through the walk (`go.mod`, root `package.json`, python manifests) deliberately stay direct `os.ReadFile` to preserve semantics. The Go repo-dominant style signals (test framework, error style, naming) are computed in ONE corpus pass (`goRepoStyleProfile` in `quality_ai_target_go.go`); each signal is an independent per-file sum, which is what makes the fold behavior-identical.
34
+
-**GOTCHA: `goFuncEligibleForUnusedCheck` must not consult `fn.Doc`.** The AI dead-code pass historically parsed with `parser.ParseFile(..., 0)`, so `fn.Doc` was always nil and the "go: directive implies external use" exemption NEVER fired. The pass now reuses the shared ParseComments corpus AST (`support.ParseGoSource`), where `fn.Doc` IS populated — keeping that check would have silently exempted unused functions carrying `//go:generate`-style comments and changed findings. It was removed to keep output identical (see the NOTE in `quality_ai_dead_code.go`); activating it is a candidate behavior fix, to be made deliberately with tests.
35
+
-**PERF: clone-detector hashing is not behavior-bearing.**`cloneToken` now stores the original-case source slice plus a per-token FNV-1a hash of the ASCII-lowercased text (tokens are ASCII-only by the regex); window grouping uses a rolling polynomial hash (`cloneWindowIndex`) and equality is verified token-by-token in `sharedCloneLength` (hash short-circuit + ASCII case-fold compare). Because every candidate is value-verified, ANY deterministic hash yields identical clone candidates — collisions only cost time. `tests/checks/quality_clone_differential_test.go` pins findings captured from the pre-rewrite algorithm; don't regenerate its expectations with the current code.
`codeguard rules` prints each rule's level, execution model, language coverage, section, and title. `codeguard explain <rule-id>` includes the same metadata for a single rule.
97
97
98
-
By default, `codeguard` looks for `codeguard.yaml`, `codeguard.yml`, or `codeguard.json` in the repository root. If those are missing, it also checks `.codeguard/codeguard.yaml`, `.codeguard/codeguard.yml`, and `.codeguard/codeguard.json`.
98
+
By default, `codeguard` looks for `codeguard.yaml`, `codeguard.yml`, or `codeguard.json` in the repository root. If those are missing, it also checks for the same file names inside a `.codeguard/` directory.
99
99
100
100
If you point `-config` at a directory such as `.codeguard`, `codeguard` will look inside it for `codeguard.*` or `config.*` files.
101
101
102
102
Text output includes ANSI color and emoji markers by default. Set `NO_COLOR=1` if you want plain terminal output.
103
103
104
-
If you want a JSON starting point instead, use [examples/codeguard.json](/Users/alex/Documents/GitHub/codeguard/examples/codeguard.json:1).
104
+
If you want a JSON starting point instead, use [examples/codeguard.json](examples/codeguard.json).
Copy file name to clipboardExpand all lines: docs/architecture.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -52,5 +52,6 @@ This keeps the runner tree organized with the same directory-first style as `int
52
52
- waivers and inline suppressions allow time-bounded exceptions
53
53
- baselines suppress known findings so new regressions are the only gate failures
54
54
- cached file findings are keyed by file hash plus config hash so repeat scans skip unchanged files
55
+
- a per-scan file corpus (`internal/codeguard/runner/support/corpus.go`) memoizes the target directory walk, individual file reads, and Go/tree-sitter parses so each file is walked, read, and parsed at most once per scan no matter how many sections inspect it; the quality AI checks share it through the `ListTargetFiles`/`ReadTargetFile`/`ParseGoFile` context hooks rather than re-reading the tree
55
56
- diff mode can scope file findings down to changed lines derived from `git diff`
@@ -969,9 +979,13 @@ When `checks.context` is omitted the family runs in full scans and is skipped in
969
979
Current behavior:
970
980
-`context.agent-docs-missing` warns once at repo level when none of the recognized agent instruction files exist at the target root
971
981
-`context.agent-docs-drift` warns when an agent instruction file references a file or directory path, a `make` target, or an npm/pnpm/yarn `run` script that provably does not exist
972
-
-`context.readme-drift` applies the same resolution to fenced `bash`/`sh`/`shell` blocks in the root README.md: `./`-prefixed paths, make targets, and run scripts that resolve nowhere
982
+
-`context.readme-drift` applies the same full extraction to the root README.md as agent docs get: prose and inline-code references as well as fenced `bash`/`sh`/`shell` blocks — the README is the doc agents and humans read first, so it earns the same truthfulness bar
973
983
-`context.oversized-context-unit` warns when a source file exceeds `context_rules.max_file_lines` (default 1500); the message is framed as agent context cost, distinct from `quality.max-file-lines` maintainability thresholds; generated and vendored files are skipped
974
-
-`context.ambiguous-symbol` warns once per source-file basename shared by at least `context_rules.ambiguous_symbol_threshold` files (default 4), listing up to five locations
984
+
-`context.ambiguous-symbol` warns once per source-file basename shared by at least `context_rules.ambiguous_symbol_threshold` files (default 4), listing up to five locations; conventional basenames in the ignore list (below) never fire
985
+
-`context.undocumented-commands` is the inverse of drift: it warns (up to 10 findings) when a high-signal Makefile target or package.json script — exactly `build`, `check`, `dev`, `fmt`, `lint`, `run`, `start`, `test`; prefixed variants like `fmt-check` are not implied — is mentioned by no agent instruction file and not even the root README. Any plausible mention counts as documentation: a structured reference (inline code, shell fence) or a plain-text `make <name>` / `npm|pnpm|yarn [run] <name>` invocation anywhere in the doc. The rule stays silent when the repo has no agent docs at all (`context.agent-docs-missing` already covers that), and when the Makefile or package.json cannot be parsed reliably (includes, pattern rules, workspaces)
986
+
-`context.oversized-agent-doc` warns when an agent instruction file exceeds `context_rules.max_agent_doc_lines` (default 600): agent docs are loaded into every session verbatim, so an oversized one consumes the context window it exists to save; the README and linked reference docs are free to be long
987
+
-`context.doc-link-rot` warns (up to 20 findings per doc) when a markdown link in an agent instruction file or the root README points at a repository file or directory that does not exist. Relative targets resolve against the doc's own directory and the repo root; absolute `/path` targets resolve against the repo root only (the hosted-viewer convention — an absolute filesystem path baked into a doc is exactly the rot this rule catches). External URLs (any scheme) are never fetched, pure `#anchor` links are skipped, `path.md#anchor` checks only the path part, editor-style `:line` suffixes are stripped, and templated or placeholder targets (`<name>`, `$VAR`, globs, `..` traversals, query strings) are exempt. Markdown links are owned by this rule; the drift rules no longer extract them, so one broken link is never reported twice
988
+
-`context.legibility-threshold` enforces the `repo_legibility` score: because legibility is good-high (the inverse of the slop score), the finding fires when the computed score falls **below** a configured floor — `warn` below `context_rules.legibility_warn_threshold`, `fail` below `context_rules.legibility_fail_threshold` (the fail level takes precedence when both match). `0` (the default) disables each threshold, and when both are set the fail threshold must be less than or equal to the warn threshold. The finding message carries the full component breakdown (e.g. `agent_docs 10/25, readme 10/10, ...`) so the weakest signal is immediately visible
975
989
976
990
Drift resolution is deliberately conservative — precision over recall. It only flags references it can positively prove broken, and skips:
977
991
- URLs, module/domain paths (`github.com/...`), absolute paths, and `..` traversals
@@ -981,16 +995,37 @@ Drift resolution is deliberately conservative — precision over recall. It only
981
995
- make targets when no root Makefile exists or the Makefile uses `include` or pattern rules
982
996
- npm scripts when there is no root package.json or it declares workspaces
983
997
998
+
Conventional-basename ignore list:
999
+
1000
+
Basenames imposed by a language or framework convention are expected to repeat, so they neither fire `context.ambiguous-symbol` findings nor count against the `navigability` score component. The default set:
Setting `context_rules.ambiguous_symbol_ignore`**replaces** the default list entirely (matching is case-insensitive): re-list the defaults plus your own names to extend it, or set it to `[]` to disable ignoring altogether.
1008
+
984
1009
`repo_legibility` artifact:
985
1010
986
1011
Every context run publishes one `repo_legibility` artifact per target with a 0-100 score (higher is more legible) and an explainable component breakdown:
987
-
-`agent_docs` (25): any agent instruction file present
1012
+
-`agent_docs` (25): agent instruction files must exist *and* have substance — credit is `25 x min(non-blank lines, 10) / 10` measured on the largest agent doc (an empty CLAUDE.md scores 0, full credit from 10 non-blank lines), minus 2 points per unresolvable reference inside the agent docs (capped at 10); the component detail spells out both terms
988
1013
-`readme` (10): root README.md present
989
-
-`doc_accuracy` (20): minus 4 points per unresolvable doc/README reference
990
-
-`context_economy` (25): scaled down by the share of source files over the context budget (10% oversized zeroes it)
991
-
-`navigability` (20): scaled down by the share of source files caught in ambiguous basename groups (20% affected zeroes it)
1014
+
-`doc_accuracy` (20): scaled by the share of doc/README references that resolve — penalty `round(20 x broken/total)`, so 2 broken of 10 costs 4 points while a doc set that is mostly wrong loses all 20; with no references the component stays at 20
1015
+
-`context_economy` (25): scaled by the share of source files over the context budget, ramping linearly to zero at 25% oversized (penalty `round(25 x share x 4)`, capped at 25)
1016
+
-`navigability` (20): scaled down by the share of source files caught in ambiguous basename groups (20% affected zeroes it), computed after removing conventional basenames per the ignore list above
1017
+
1018
+
The artifact is emitted even when individual rules are toggled off, so the score always reports reality. The `context.legibility-threshold` rule (above) turns the score into a warn/fail gate.
1019
+
1020
+
Score history:
1021
+
1022
+
The legibility score trend is persisted per target next to the scan cache (`<cache>.legibility-history.<ext>`, e.g. `cache.legibility-history.json`) whenever the cache is enabled; subsequent scans annotate the artifact with `previous_score` and `delta`. `context_rules.legibility_history: false` disables persistence and `context_rules.legibility_history_limit` caps retained entries per target (default 100). Print the recorded trend with:
0 commit comments