Skip to content
ChangeGuard

Evaluationruleset 2026.10.4dataset b65381532aba…generated 2026-10-08

What the evaluation shows, and what it cannot.

67 labelled code changes, two naive baselines, an ablation without the repository snapshot, and three local language models, all scored with one-to-one matching and bootstrap intervals over cases. The dataset is small and synthetic, and the same author wrote it and the rules. Read every number with that in mind.

Cases
67
33 dev · 21 holdout · 13 holdout v2
Labels
65
where a reviewer should look
Negative controls
9
safe changes; any finding is a false alarm
Bootstrap
2,000
resamples over cases, seed 20261008
1 / 08Clean holdout results

1Clean holdout results

Start with the numbers nothing was tuned on.

The rules were written against the dev split. Two holdout sets were written afterwards, with the ruleset frozen, and each was run exactly once before anything changed. Those first runs are the closest thing here to an unbiased estimate. Every fix they prompted is listed, and later numbers on the same split no longer count as clean.

holdout · first run · ruleset 2026.10.12026-10-08

Precision

  • ChangeGuard83.3%
  • ChangeGuard, diff only90.9%
  • Keyword search baseline57.1%
  • Flag-every-change baseline27.3%

Recall

  • ChangeGuard75.0%
  • ChangeGuard, diff only50.0%
  • Keyword search baseline40.0%
  • Flag-every-change baseline15.0%

False alarms on safe changes (ChangeGuard): 67% of 3 negative controls.

Fig. 1Holdout, first run: 21 cases, 20 labels, 3 negative controls, ruleset 2026.10.1. Point estimates only; raw console output is kept in eval/reports/runs/holdout-first-run-ruleset-2026.10.1.txt.
holdout-v2 · first run · ruleset 2026.10.22026-10-08

Precision

  • ChangeGuard100.0%
  • ChangeGuard, diff only100.0%
  • Keyword search baseline42.9%
  • Flag-every-change baseline42.9%

Recall

  • ChangeGuard81.8%
  • ChangeGuard, diff only63.6%
  • Keyword search baseline27.3%
  • Flag-every-change baseline27.3%

False alarms on safe changes (ChangeGuard): 0% of 2 negative controls.

Fig. 2Holdout v2, first run: 13 cases, 11 labels, 2 negative controls, ruleset 2026.10.2. Point estimates only; raw console output is kept in eval/reports/runs/holdout-v2-first-run-ruleset-2026.10.2.txt.

Changed after the holdout first run

  • Fixed: JavaScript/TypeScript methods were treated as having an implicit self parameter, hiding new required parameters (ho-ts-method-001).
  • Fixed: adding a return annotation where none existed was reported as a return-type change (ho-py-neg-logging-001).
  • Changed: pure renames and private helpers whose same-file callers are tested no longer produce test-gap findings (ho-ts-neg-rename-001).
  • Added: yaml.unsafe_load / marshal.loads / jsonpickle.decode detection, not covered by Ruff (ho-py-unsafe-load-001).
  • Label correction: an omitted acceptable test-gap entry was added to ho-py-dataclass-001, per LABELING.md section 4.

Changed after the holdout v2 first run

  • Added: verify=False passed through Session/Client methods (Ruff S501 only matches module-level requests calls) (h2-py-session-verify-001).

2All cases, current ruleset

All 67 cases with the current rules, intervals included.

These numbers include the dev split the rules were written against and the holdout cases after their fixes, so they are optimistic by construction. The intervals are 95% percentile bootstrap intervals over cases; with this few cases they are wide, and a perfect point estimate still comes with a lower bound.

The ablation is the most informative row. Without the repository snapshot, the same rules keep their precision but recall falls from 93.8% to 72.3%: the missing share is what cross-file analysis (callers, imports, tests) contributes.

all splits · current ruleset67 cases · 65 labels

Precision

95% CI
  • ChangeGuard100.0%
  • ChangeGuard, diff only100.0%
  • Keyword search baseline59.3%
  • Flag-every-change baseline31.4%

Recall

95% CI
  • ChangeGuard93.8%
  • ChangeGuard, diff only72.3%
  • Keyword search baseline49.2%
  • Flag-every-change baseline16.9%
Fig. 3Overall precision and recall per system, with 95% bootstrap intervals over cases.
changeguard · by split

Precision

95% CI
  • Dev100.0%
    34 TP · 0 FP · 0 FN
  • Holdout100.0%
    17 TP · 0 FP · 3 FN
  • Holdout v2100.0%
    10 TP · 0 FP · 1 FN

Recall

95% CI
  • Dev100.0%
    34 TP · 0 FP · 0 FN
  • Holdout85.0%
    17 TP · 0 FP · 3 FN
  • Holdout v290.9%
    10 TP · 0 FP · 1 FN
Fig. 4ChangeGuard (no AI) by split. The dev split is where the rules were developed.
All systems: precision, recall and F1 with 95% intervals, negative-control false-positive rate.
SystemPrecision [95% CI]Recall [95% CI]F1False alarms on safe changes
ChangeGuardDeterministic + heuristic rules (no AI)100.0% [100–100]93.8% [88–99]96.8%0% (0/9)
+ llama3.2:3bRules + AI synthesis (ollama/llama3.2:3b, replay)98.4% [95–100]93.8% [88–99]96.1%0% (0/9)
+ qwen2.5-coder:7bRules + AI synthesis (ollama/qwen2.5-coder:7b, replay)53.4% [49–58]96.9% [92–100]68.8%100% (9/9)
+ qwen2.5-coder:7b, 3-sample voteRules + AI synthesis (ollama/qwen2.5-coder:7b, 3-sample self-consistency, replay)65.0% [59–71]96.9% [92–100]77.8%78% (7/9)
ChangeGuard, diff onlySame rules without the repository snapshot (ablation)100.0% [100–100]72.3% [61–83]83.9%0% (0/9)
Keyword search baselineRegex search for risky-looking tokens on added lines59.3% [46–73]49.2% [37–61]53.8%44% (4/9)
Flag-every-change baselineFlag every changed function (signature changes as breaking)31.4% [16–50]16.9% [8–27]22.0%100% (9/9)

3What it misses

4 labels missed, and why no rule caught them.

Each miss below is a change whose problem lies in its meaning rather than its shape. Nothing syntactic distinguishes these lines from a correct edit, so a rule that flagged them would also flag a great many correct edits.

This is where a language model could earn its place, and it partly does: qwen2.5-coder:7b found 2 of these 4. The section on language models shows what that cost.

  • h2-py-naive-datetime-001Holdout v2L1

    UTC timestamps become naive local time

    Audit timestamps lose their timezone; ordering across hosts in different zones breaks. No syntactic rule targets this.

    Behaviour changesrc/audit/stamp.py:5

    Not found by any evaluated system.

  • ho-py-semantic-001HoldoutL1

    Operands swapped in a refund calculation

    The subtraction is reversed, so refunds become negative. No syntactic rule targets this.

    Logic changesrc/billing/refunds.py:2

    Found by qwen2.5-coder:7b; qwen2.5-coder:7b (3-sample vote)

  • ho-py-semantic-002HoldoutL1

    Quantity dropped from a total during a refactor

    While adding a discount, the per-line quantity multiplication disappears; the only test uses quantity 1 and still passes.

    Logic changesrc/cart/totals.py:13

    Found by qwen2.5-coder:7b; qwen2.5-coder:7b (3-sample vote); Keyword search baseline

  • ho-py-test-deleted-001HoldoutL2

    Password tests deleted while the KDF cost is halved

    The only tests for password verification are deleted in the same change that halves the PBKDF2 iteration count.

    Securitysrc/auth/password.py:6

    Not found by any evaluated system.

4Language models

Models ground their claims. Most of their new risks are still wrong.

Each model ran locally through ollama on all 67 cases with the same prompt (v1.0.0), greedy decoding, and a fixed seed. Every response is recorded, so these numbers replay exactly in CI without a model. The vote row samples three answers at temperature 0.7 and keeps a proposed risk only if a majority of samples agree.

Two results hold across models. First, grounding works: no system cited evidence that failed the independent re-check, and the verifier rejected the claims that named findings, paths, or symbols the evidence did not support. Second, grounded is not the same as right. The larger model proposed many additional risks that cite real evidence and still match no label. That is why model findings sit in their own lane, are capped at medium confidence, and can never fail a build.

ai synthesis · replayedprompt v1.0.0

Precision

95% CI
  • ChangeGuard100.0%
  • + llama3.2:3b98.4%
  • + qwen2.5-coder:7b53.4%
  • + qwen2.5-coder:7b, 3-sample vote65.0%

Recall

95% CI
  • ChangeGuard93.8%
  • + llama3.2:3b93.8%
  • + qwen2.5-coder:7b96.9%
  • + qwen2.5-coder:7b, 3-sample vote96.9%
Fig. 5Rules alone against rules plus each model. Recall rises slightly with the larger model; precision falls with every additional model-proposed risk that matches no label.
Per-model AI statistics: claims, rejections, model-proposed findings, cost.
ModelClaims (rejected)Notes attachedModel-proposed risks (matched a label)False alarms on safe changesMean latency · tokens in/out
llama3.2:3b190 (20)971 (0)0%4.9 s · 986/340
qwen2.5-coder:7b241 (3)10157 (2)100%13.6 s · 1016/436
qwen2.5-coder:7b@3241 (26)10136 (2)78%40.2 s · 3049/1305

llama3.2:3b

Why claims were rejected

  • unknown_finding14
  • symbol_not_in_evidence5
  • no_evidence1
  • unknown_path1

qwen2.5-coder:7b

Why claims were rejected

  • unknown_path2
  • symbol_not_in_evidence1

qwen2.5-coder:7b@3

Why claims were rejected

  • low_agreement23
  • unknown_path2
  • symbol_not_in_evidence1

Self-consistency helps, at a price. Voting over three samples dropped 21 of 57 model-proposed risks while keeping every one that matched a label, at roughly three times the tokens. The smaller model is quieter: it rarely proposes risks and mostly attaches explanations to findings the rules already made.

Not measured: whether the explanations are correct or useful. That needs human rating, and none has been done.

5Provenance and severity

Precision by how a finding was established.

Deterministic and heuristic findings are counted separately, as are confidence levels. A finding is acceptable when the case lists it as a reasonable extra observation, or when it is a second finding on a label already matched; acceptable findings count as neither true nor false positives. Most heuristic findings land there: test gaps a reviewer would agree with, but that the labels do not require.

ChangeGuard precision by provenance and by confidence.
GroupTrue positivesFalse positivesAcceptablePrecision
provenance: deterministic5001100.0%
provenance: heuristic11040100.0%
confidence: high5200100.0%
confidence: medium9041100.0%

Severity agreement on matched findings

Exact
93.4%
Within one level
100.0%

Severity is a categorical judgement per rule, compared with the labeller’s. It is not a calibrated probability and is never presented as one.

ChangeGuard recall by label category.
CategoryLabelsFoundRecall
Behaviour change2150.0%
Logic change5360.0%
Security161593.8%
Breaking change1515100.0%
Dependency55100.0%
Test integrity55100.0%
Data migration44100.0%
Error handling44100.0%
Concurrency33100.0%
Correctness22100.0%
Secret exposure22100.0%
Coverage gap11100.0%
Prompt injection11100.0%

6Safe changes and evidence

Silence on safe changes, and evidence that checks out.

9 cases are safe changes with no label at all, so any finding on them is a false alarm. A review tool that cries wolf gets ignored, which makes this the number reviewers feel most.

Separately, an independent checker re-reads each case’s own files and verifies every evidence item a finding cites: it must exist, point at lines that exist in the stated revision, and quote those lines verbatim. A finding with any evidence that fails counts as unsupported.

negative controls9 safe changes
  • ChangeGuard0/9
  • + llama3.2:3b0/9
  • + qwen2.5-coder:7b9/9
  • + qwen2.5-coder:7b, 3-sample vote7/9
  • ChangeGuard, diff only0/9
  • Keyword search baseline4/9
  • Flag-every-change baseline9/9
evidence integrity · independent re-check
ChangeGuard0 of 102 unsupported
+ llama3.2:3b0 of 103 unsupported
+ qwen2.5-coder:7b0 of 159 unsupported
+ qwen2.5-coder:7b, 3-sample vote0 of 138 unsupported
ChangeGuard, diff only0 of 48 unsupported

7Method

How the numbers are produced.

Dataset

67 cases, each a patch with the repository before the change and, for some, a coverage report. Each case lists expected labels (category, file, lines, severity) and acceptable extra observations. Labelling rules are in eval/LABELING.md.

Matching

Same category and file, line ranges overlapping within ±3 lines, one-to-one. Precision and recall are micro-averaged over all labels.

Intervals

95% percentile bootstrap over cases, 2,000 resamples, seed 20261008, overall and per split.

Baselines

A keyword search over added lines, and a system that flags every changed function. The diff-only ablation runs the same rules without the repository snapshot.

Language models

Local inference through ollama, temperature 0 and seed 7. Responses are recorded per request fingerprint and replayed exactly.

Regression gate

CI replays the evaluation against a committed baseline. It fails if precision, recall or F1 drops by more than 2 points, the false-alarm rate rises by more than 5, unsupported evidence increases, or a previously found label is missed.

Reproduce (no model needed: responses are replayed)
cd backendMODELS="llama3.2:3b,qwen2.5-coder:7b,qwen2.5-coder:7b@3"uv run changeguard eval --ai replay \  --ai-model "$MODELS" \  --check-baseline ../eval/baselines/changeguard.json
run manifest
engine0.1.0 · ruleset 2026.10.4
dataset sha256b65381532aba57ce7cb625b1b2c540b2c26b03c173567dd59abfee08cd456b7f
prompt sha256459e33c66e82d4b710050d5c1444386384f8eee4e969089c172c65b6ff64726b
python3.12.12
platformmacOS-26.6.2
machinearm64
ruffruff 0.16.10
generated2026-10-08T16:43:36+00:00

8Limitations

What these numbers do not say.

  1. 01The dataset is small and synthetic: each case was written for this project to exercise a specific risk pattern. Results measure agreement with the labelling guideline on these cases, not effectiveness on real repositories.
  2. 02The rules and the dev-split cases were written by the same author, so dev-split numbers are optimistic by construction. The holdout splits were written after the ruleset was frozen; their first runs, recorded before any holdout-informed change, are the more honest estimate, but they share the same author and style.
  3. 03Bootstrap intervals resample cases; with this few cases they are wide and should be read as rough bounds.
  4. 04Labels mark where a reviewer should look, not whether the change causes a production incident. Nothing here estimates incident probability.
  5. 05AI metrics measure grounding (cited evidence exists and matches) and agreement with labels. They do not measure the correctness or usefulness of explanations, which would require human rating.
Per-case results for ChangeGuard (67 cases)
CaseSplitFoundMissedFalse positives
h2-js-describe-skip-001holdout-v2L1——
h2-py-flask-debug-001holdout-v2L1——
h2-py-jwt-001holdout-v2L1——
h2-py-kwonly-001holdout-v2L1——
h2-py-naive-datetime-001holdout-v2—L1—
h2-py-neg-validation-001safeholdout-v2———
h2-py-session-verify-001holdout-v2L1——
h2-py-staticmethod-001holdout-v2L1——
h2-py-timeout-handling-001holdout-v2L1——
h2-sql-rename-column-001holdout-v2L1——
h2-ts-neg-extract-001safeholdout-v2———
h2-ts-optional-required-001holdout-v2L1——
h2-tsx-innerhtml-001holdout-v2L1——
ho-js-commonjs-001holdoutL1——
ho-js-neg-reduce-001safeholdout———
ho-py-alias-module-001holdoutL1——
ho-py-async-sleep-001holdoutL1——
ho-py-dataclass-001holdoutL1——
ho-py-diffonly-tls-001holdoutL1——
ho-py-injection-001holdoutL1, L2——
ho-py-moved-module-001holdoutL1——
ho-py-mutable-default-001holdoutL1——
ho-py-neg-logging-001safeholdout———
ho-py-pickle-001holdoutL1——
ho-py-popen-shell-001holdoutL1——
ho-py-semantic-001holdout—L1—
ho-py-semantic-002holdout—L1—
ho-py-test-deleted-001holdoutL1L2—
ho-py-unsafe-load-001holdoutL1——
ho-sql-delete-all-001holdoutL1——
ho-ts-constructor-001holdoutL1——
ho-ts-method-001holdoutL1——
ho-ts-neg-rename-001safeholdout———
ho-ts-new-function-001holdoutL1——
js-error-001devL1——
js-logic-001devL1——
js-security-001devL1——
json-deps-001devL1, L2, L3——
py-async-001devL1——
py-behavior-001devL1——
py-breaking-001devL1——
py-breaking-002devL1——
py-breaking-003devL1——
py-breaking-004devL1——
py-coverage-001devL1——
py-deps-001devL1, L2——
py-error-001devL1——
py-error-002devL1——
py-logic-001devL1——
py-logic-002devL1——
py-migration-001devL1——
py-neg-001safedev———
py-neg-002safedev———
py-neg-003safedev———
py-secret-001devL1——
py-security-001devL1, L2——
py-security-002devL1——
py-test-001devL1, L2——
py-undefined-001devL1——
sql-migration-001devL1——
ts-async-001devL1——
ts-breaking-001devL1——
ts-breaking-002devL1——
ts-neg-001safedev———
ts-security-001devL1——
ts-security-002devL1——
ts-test-001devL1——

Want to see what a single analysis looks like? Walk through one change or read how each rule works.