Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .claude/knowledge/architecture-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ Key architectural decisions, service boundaries, data flow, integration points,
- **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.
- **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.
- **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.

- **Finding identity excludes presentation.** Exact, context, and content fingerprints derive from rule ID, path, line, and source identity only (`runner/support/findings_section.go`) — never message prose, metadata, confidence, or the *reported* level. Confidence demotion (`checks.confidence_demotion`) rewrites `Level`/`Severity` after `NewFinding`, so a demoted finding still matches its baseline entry. Any future policy that changes how a finding is reported must preserve this.
- **Post-cache policy must be stripped from cache fingerprints.** `SectionConfigHashes` calls `findingRelevantChecks` to zero settings that cannot change per-file findings (currently `checks.min_confidence` and `checks.confidence_demotion`), including for the conservative `""` all-checks family. Filtering happens in `FinalizeSectionWithDiagnostics`, i.e. after `cachedFileFindings`, so a threshold change must re-render a scan, never invalidate cached findings. Add any new finalize-time-only setting to that strip list.
- **Section ids diverge between the registry and reports.** `runner/checks/registry.go` uses `supply-chain`, but the section finalizes as `supply_chain`, which is what appears in `SectionResult.ID`, in `checks.disabled`, and in `checks.min_confidence.sections`. `core.SectionKeys()` holds the report-facing list; `tests/codeguard` asserts every id a real scan reports is in it.
- **`config.WriteFile` stamps `codeguard_version`, `ApplyDefaults` does not.** The stamp is applied at the write boundary so a *loaded* config keeps whatever its file recorded — that is what makes it useful for spotting a config produced by another release. It is never validated (any value, including an unparseable one, must still load), and it is absent from `SectionConfigHashes`, so a release upgrade cannot discard cached findings. Release builds write `v{{.Version}}` (GoReleaser `-X internal/version.Number=v{{.Version}}`), so hand-maintained configs use the `v`-prefixed form; a local dirty build writes e.g. `v1.9.0+dirty`.

14 changes: 13 additions & 1 deletion .claude/knowledge/testing-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Testing strategies, test infrastructure quirks, how to run/debug specific test s

- The trust policy (`internal/codeguard/trust`) is a process-global. Tests that exercise command-driven checks or local (127.0.0.1) AI endpoints must enable it. `tests/checks` and `tests/codeguard` each have a `TestMain` (`trust_main_test.go`) that sets `Policy{AllowConfigCommands: true, AllowConfigAIEndpoints: true}`. If you add a new test package that runs config commands or hits a loopback test server, add a similar `TestMain` or the tests will fail with "refusing to run config-supplied command" / "ssrf guard" errors.
- `tests/security` deliberately has NO trust `TestMain`: it verifies the secure *default*. Tests there set/restore the policy locally with `trust.Set(...)` + `t.Cleanup(trust.ResetFromEnv)`. Do not add a package-wide opt-in there.
- All tests live under `tests/` as external (`_test`) packages; there are no white-box tests inside `internal/`/`pkg/`. Internal packages are still importable from `tests/` because they are within the module. This is enforced by `ci.test-file-location` (the repo's own scan fails on `_test.go` files outside `tests/**`), so do not add tests next to the code even for unexported access — import the internal package from `tests/` and test via its exported API instead.
- New tests go under `tests/` as external (`_test`) packages. A small number of legacy in-package tests do exist (e.g. `internal/codeguard/runner/support/corpus_memory_test.go`, `internal/codeguard/runner/pr_summary_test.go`, `internal/codeguard/config/profile_test.go`) and are permitted because `ci_rules.allowed_test_paths` in `.codeguard/codeguard.yaml` lists `internal/**` and `cmd/**` alongside `tests/**` — so the self-scan will NOT catch a new in-package test. The `tests/`-only rule is convention enforced by review, not by the scan; extend those existing files when you need unexported access, don't add new ones. Internal packages are still importable from `tests/` because they are within the module. This is enforced by `ci.test-file-location` (the repo's own scan fails on `_test.go` files outside `tests/**`), so do not add tests next to the code even for unexported access — import the internal package from `tests/` and test via its exported API instead.
- **Never commit a contiguous real-format secret as a test fixture.** GitHub push protection rejects the push (it flags GitLab/Stripe/SendGrid/Twilio/etc. tokens even in `_test.go` files), and it is poor practice in a secret-detection repo. Assemble fixtures at runtime from a prefix + body via the `cred(prefix, body)` helper in `tests/checks/test_helpers_test.go` (e.g. `cred("AKIA", "1234567890ABCDEF")`), splitting the recognizable provider prefix from the rest so no full token literal appears in source. The reconstructed value still exercises the scanner identically. `goConst(value)` wraps a value as a minimal Go source fixture.
- The MCP server smoke tests (`tests/mcp/`) drive the **real binary in a subprocess**, not the in-process handler, to keep tests external. Pattern: a `Test...HelperProcess` func gated by an env var (`GO_WANT_MCP_HELPER_PROCESS` for stdio, `GO_WANT_MCP_HTTP_HELPER_PROCESS` for HTTP) re-execs `os.Args[0]` and calls `cli.Run(...)`. stdio replays NDJSON transcripts over stdin; HTTP reserves a free `127.0.0.1:0` port, launches `serve --mcp --http`, polls `/healthz` for readiness, then issues real HTTP requests. Add new MCP behavior as a transcript + assertion (stdio) or a subtest in `http_test.go` (HTTP).
- **Detector precision corpus** (`tests/corpus/`): fixtures live under `testdata/<language-group>/<rule>/{vulnerable,clean}/` with ground truth in `expectations.yaml`. `known_gaps` entries assert a documented FP/FN *still exists* — when you improve a detector the corpus test fails on purpose with a "promote it to must_fire / remove the known_gaps entry" message; update the manifest in the same change. Fixture credentials must be synthetic but pattern-shaped (see the secret-fixture rule above).
Expand All @@ -14,3 +14,15 @@ Testing strategies, test infrastructure quirks, how to run/debug specific test s
- **TS tests can be hijacked by the Node semantic engine**: on hosts with a discoverable `typescript.js` (e.g. VS Code installed), TypeScript targets route through the Node runner instead of the per-file Go path. Tests that must exercise the per-file path (tree-sitter differential tests, corpus TS groups) set `CODEGUARD_TYPESCRIPT_LIB_PATH` to an existing-but-invalid lib to force the fallback.
- Defensive precision positive fixtures should avoid UI-ish names such as `render*` unless the test is explicitly covering UI suppression. The defensive boundary/null rules intentionally skip React/UI helper contexts, so a fixture named like a renderer can stop emitting the server-side defensive finding the test expects.
- Precision-rule retunes should include both the false-positive fixture and a nearby positive that still fires. Dogfood the local binary against a real target repo before committing; several naming/mutation rules only showed meaningful movement after scanning a mixed React, route, and integration-heavy TypeScript monorepo.
- **`go test ./...` is red on `main` (as of 1205484).** `TestSecuritySemanticAnalyzerScansNodeModulesWithinTarget` (`tests/checks/typescript_semantic_test.go:182`) fails with `Security status = "pass", want "fail"`. Its `requireTypeScriptSemanticRuntime` guard does *not* skip, so this is a real failure, not a missing-runtime skip; it plausibly relates to the vendored/`node_modules` scan bounding in 24cb12c but has not been diagnosed. Reproduce a baseline in a clean worktree (`git worktree add --detach <dir> HEAD`) before attributing a `tests/checks` failure to your own change.
- **Local strict lint needs a pinned Go toolchain, or it silently under-reports.** Plain `golangci-lint run` fails with 2 `typecheck` issues inside `crypto/internal/randutil` / `math/rand/v2`: the binary is built with go1.26.3 but the Homebrew toolchain is 1.27.1, whose stdlib it cannot parse. It still exits 0, and — the dangerous part — `staticcheck` findings in your own code are **suppressed** when typecheck fails, so a clean-looking local run can still fail CI. Reproduce CI exactly with:

```
env -u GOROOT GOTOOLCHAIN=go1.26.3 GOCACHE=/private/tmp/codeguard-go-cache \
GOLANGCI_LINT_CACHE=/private/tmp/golangci-lint-cache /Users/alex/.go/bin/golangci-lint run
```

Match `GOTOOLCHAIN` to the Go version the linter was built with (`golangci-lint --version`), not to go.mod. CI pins golangci-lint v2.12.2 with `go-version-file: go.mod`.
- **Report rendering is tested through the SDK.** `internal/codeguard/report` has no external test hooks, so `tests/report` drives it via `codeguard.WriteReport(w, report, format)` with a hand-built `codeguard.Report`. Note the text writer emits a large braille/ANSI banner, so assert on substring *positions* rather than diffing whole output. `tests/core` and `tests/report` were added as external packages for `core` and `report` respectively.
- **Verifying a self-scan didn't add findings:** `make codeguard-ci` prints only a summary count, so compare against a clean worktree at HEAD and diff the `N. at:` location lines (`grep -E '^\s+[0-9]+\. at:' | sed 's/^ *[0-9]*\. at: //' | sort`). The repo's own `quality.ai.narrative-comment` rule fires on doc comments that describe what code is rather than why it exists, so new comments can add findings.

1 change: 1 addition & 0 deletions .codeguard/codeguard.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
codeguard_version: v1.9.0
name: codeguard-repo-ci
exclude:
- .claude/**
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

`codeguard` is a standalone Go service and CLI for repository checks across code quality, production reliability, data correctness, design boundaries, security, CI/CD hygiene, AI prompt governance, and repo-specific policy rules.

It now supports repository exclusions, baselines, waivers, changed-lines diff scans, SARIF output, GitHub annotations, custom rule packs, natural-language custom rules through an optional AI runtime, policy profiles, scan caching, doctor checks, rule discovery from the CLI, native TypeScript/Python quality, design, security, reliability, and data-correctness heuristics, and language-specific command checks.
It now supports repository exclusions, baselines, waivers, changed-lines diff scans, SARIF output, GitHub annotations, custom rule packs, natural-language custom rules through an optional AI runtime, policy profiles, per-section minimum-confidence policy, scan caching, doctor checks, rule discovery from the CLI, native TypeScript/Python quality, design, security, reliability, and data-correctness heuristics, and language-specific command checks.

For a user-facing glossary of every check family and subsection, see [docs/checks.md](docs/checks.md).

Expand Down
23 changes: 23 additions & 0 deletions docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,29 @@ Tree-sitter parsing (opt-in):
- oversized files (> 256 KiB), parse failures, and error-heavy trees fall
back to the regex path per file

Confidence policy (opt-in):
- every finding carries a confidence of `high`, `medium`, or `low`; an
unspecified confidence is treated as `medium`. Structural analyses set
`high` (source-to-sink taint, tree-sitter rule paths), while regex line
scans generally leave it unspecified
- `checks.min_confidence.default` drops findings below the given level, and
`checks.min_confidence.sections.<section>` overrides it for one section.
Section keys are the ids reported in scan output and accepted by
`checks.disabled` (supply chain is `supply_chain`). Omitting the block, or
setting `low`, admits every finding — the default behavior
- removed findings are never silent: each is counted per rule as
`confidence_filtered` in the `rule_stats` artifact, reported per section as
`confidence_filtered_count`, and listed with reason `confidence` under
`--include-suppressed`. They are excluded from `suppression_ratio`, which
keeps meaning "findings teams work around"
- `checks.confidence_demotion: true` reports a `low`-confidence finding on a
failing rule as a warning instead. It never promotes and never applies to
`medium` or `high`. Off by default
- filtering and demotion are applied when a section is finalized, after the
per-file findings cache, so changing either setting re-renders a scan
instead of re-running one, and neither affects a finding's fingerprint or
its baseline match

## Quality

Purpose:
Expand Down
58 changes: 58 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,64 @@ JSON:
}
```

### Config provenance

Every config codeguard writes (`codeguard init`, `WriteConfigFile`) records the
release that produced it as a top-level `codeguard_version`. Loading reports
what the file says rather than the running binary, so a config written by a
different release is visible on inspection.

The stamp is provenance only: it is never validated, so a config written by an
older or newer release always loads, and it is excluded from the per-file cache
fingerprints, so a release upgrade does not discard cached findings.

```yaml
codeguard_version: v1.9.0
name: my-repo
```

### Minimum confidence policy

Drop findings the scanner is not confident enough about, globally or per
section, and optionally report low-confidence failures as warnings. Omitting
the block admits every finding, which is the default behavior. Section keys are
the ids reported in scan output (the same names `checks.disabled` accepts).

Both settings are applied when a section is finalized, after the per-file
findings cache, so changing a threshold re-renders a scan rather than
re-running one, and neither changes a finding's fingerprint or its baseline
match. Every removed finding is counted per rule as `confidence_filtered` and
per section as `confidence_filtered_count`.

YAML:

```yaml
checks:
min_confidence:
default: medium
sections:
security: high
quality: low
confidence_demotion: true
```

JSON:

```json
{
"checks": {
"min_confidence": {
"default": "medium",
"sections": {
"security": "high",
"quality": "low"
}
},
"confidence_demotion": true
}
}
```

### Enable AI change risk

YAML:
Expand Down
Loading
Loading