Skip to content

feat: baseline diff + quality gate (Own.NET Auditor phase 2) - #8

Merged
PhysShell merged 3 commits into
mainfrom
claude/sts-runtime-analysis-2mo4z9
Jun 26, 2026
Merged

feat: baseline diff + quality gate (Own.NET Auditor phase 2)#8
PhysShell merged 3 commits into
mainfrom
claude/sts-runtime-analysis-2mo4z9

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Фаза 2 дорожной карты — debt-ратчет

Гейт «валим только за новый мусор», не за ~72k принятого легаси. Продолжает Фазу 1 (SARIF-экспортёр) из #7.

report/baseline.py — diff + gate

Сравнивает сохранённый baseline с текущими находками → new / fixed / net + severity-aware гейт.

Идентичность находки — тот же стабильный fingerprint, что у SARIF-экспортёра (rule + path + нормализованное сообщение, per-occurrence disambiguated). Поэтому:

  • гейт и GitHub code-scanning алерты сходятся в том, что считать «той же находкой»;
  • сдвиг строки или смена идентификаторов/чисел в сообщении не считается новой находкой.

Это set-difference по всему корпусу — намеренно не diff_findings из fix-arm (тот distance-based, для before/after одного применённого фикса на одном дереве, а не для сравнения двух прогонов аудита).

report/diff_cli.py — CLI

# снять baseline из известно-хорошего прогона (один раз, на стенде):
python3 -m report.diff_cli --save-baseline --baseline sts_audit/baseline.json

# гейт нового прогона (CI):
python3 -m report.diff_cli --baseline sts_audit/baseline.json
#   exit 0 — нет нового долга ≥ --gate-level; exit 2 — есть
  • --save-baseline пишет компактный record (fingerprint + rule/path/cat/tool, без длинных сообщений);
  • гейт пишет diff.json + diff.md; --gate-level note|warning|error (дефолт warning); --report-only никогда не валит сборку.

Семантика ратчета

Гейт смотрит на новизну, а не на net: можно починить одну находку и внести другую (net 0) — сборка справедливо упадёт за новую.

Test plan

PYTHONPATH=. python3 report/tests/test_baseline.py   # 7/7 (new/fixed, line-shift≠new, multiset, gate, round-trip, CLI)
PYTHONPATH=. python3 report/tests/test_sarif.py      # 14/14 (без изменений)

Зелёные под python3 и python3 -O.

Проверено на реальных 72 569 находок: self-diff = 0 new / 0 fixed → PASS (exit 0); сэмулированный прогон (+1 CA2000 error, −1 находка) = 1 new / 1 fixed / net +0, но гейт FAIL (exit 2) — новый долг error-уровня валит сборку.

Baseline (~13MB на 72k) — пользовательский артефакт, создаётся на стенде, в .gitignore. Дока docs/own-net-auditor.md §3 — Фаза 2 помечена ✅.

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added Own.NET Auditor “Baseline + diff” workflow with a severity-based quality gate for newly introduced findings.
    • Generates diff.json and diff.md, supports saving a baseline, and returns a non-zero exit code when the gate fails (unless report-only mode is used).
  • Bug Fixes
    • Improved finding matching so line shifts don’t create false “new” results, and duplicates are handled correctly; missing baseline findings are reported as “fixed”.
  • Documentation
    • Updated guidance describing baseline/diff behavior and CI gate logic.
  • Tests
    • Added a validation suite covering baseline/diff, gating rules, and CLI exit-code behavior.
  • Chores
    • Updated .gitignore to exclude the user-managed baseline artifact.

A debt ratchet — fail CI on NEW findings only, never the ~72k accepted legacy.

- report/baseline.py: diff a saved baseline against current findings into
  new/fixed/net, and a severity-aware gate. Finding identity is the SAME stable,
  line-independent fingerprint the SARIF exporter uses (rule + path + normalized
  message, per-occurrence), so the gate and GitHub's code-scanning alerts agree
  on what's "the same finding". Whole-corpus set-difference, not the fix-arm's
  distance-based diff_findings (which is for a single fix's before/after).
- report/diff_cli.py: --save-baseline writes a compact, commit-friendly record
  (fingerprint + rule/path/cat/tool, no long messages); the gate writes
  diff.json + diff.md and exits 2 when a NEW finding is at/above --gate-level
  (default warning; --report-only never fails). A line shift or an
  identifier/count change in the message is not counted as new.
- 7 tests (new/fixed, line-shift-not-new, duplicate multiset, gate by level,
  compact-record round-trip, CLI save+gate), green under -O.

Verified on the real 72,569-finding corpus: self-diff = 0/0 PASS; a simulated
run (+1 error, -1) = 1 new / 1 fixed / net 0 but gate FAILS (new error-level
debt). Baseline is a user-managed artifact (created on the stand), gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLHz4cevQyXHKZnjzCqhoa
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cae0fa22-b842-477d-b6bd-fe5e8130c3f6

📥 Commits

Reviewing files that changed from the base of the PR and between e661e93 and b18b453.

📒 Files selected for processing (2)
  • report/diff_cli.py
  • report/tests/test_baseline.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • report/diff_cli.py
  • report/tests/test_baseline.py

📝 Walkthrough

Walkthrough

Adds fingerprint-based baseline diffing for audit findings, a CLI to save or compare baselines, JSON and Markdown diff outputs, severity gating, updated docs, an ignore rule for the baseline artifact, and tests for the diff and CLI flow.

Changes

Own.NET Auditor baseline diff and gate

Layer / File(s) Summary
Fingerprint records
report/baseline.py
occurrence_fingerprints builds per-occurrence identities, baseline_record serializes them into baseline records, and _baseline_fps accepts saved records or raw findings.
Diff and gate logic
report/baseline.py
diff compares baseline and current fingerprints into new/fixed sets with net, gate filters blocking new findings by severity, and summarize counts levels and tiers.
CLI report flow
report/diff_cli.py, docs/own-net-auditor.md, .gitignore
The CLI loads current findings, saves a baseline when requested, otherwise writes diff.json and diff.md from the diff/gate result, and the docs and ignore rule describe the baseline artifact.
Baseline and CLI tests
report/tests/test_baseline.py
Tests cover fingerprint matching, duplicate handling, gate thresholds, record round-tripping, save-baseline and report-only CLI paths, and the direct runner.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I hopped by the baseline, neat and bright,
New findings now glow in careful light.
A gate by severity, tidy and true,
With diff and report, I scamper through.
🥕 Hop-hop—safe debt, and carrots too!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: phase 2 adds baseline diff and a quality gate for Own.NET Auditor.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sts-runtime-analysis-2mo4z9

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@report/diff_cli.py`:
- Around line 26-28: The `_load_findings` helper currently lets a missing or
malformed `--current` file raise uncaught `FileNotFoundError` or `KeyError`,
unlike the baseline path handling in the CLI. Update `_load_findings` in
`diff_cli.py` to validate the file load and the presence of the "findings" key,
then surface a clean user-facing error and exit consistently with the existing
graceful failure path used by the main CLI logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0104bea4-537f-483d-b222-d484705ba7c0

📥 Commits

Reviewing files that changed from the base of the PR and between 7a34095 and ff2b0af.

📒 Files selected for processing (5)
  • .gitignore
  • docs/own-net-auditor.md
  • report/baseline.py
  • report/diff_cli.py
  • report/tests/test_baseline.py

Comment thread report/diff_cli.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff2b0afc3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread report/baseline.py
Comment on lines +27 to +30
base = _fingerprint(f)
k = seen.get(base, 0)
seen[base] = k + 1
out[base if k == 0 else f"{base}/{k}"] = f

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track duplicate locations before gating new debt

In occurrence_fingerprints, findings with the same rule/path/normalized message are reduced to the indistinguishable keys base, base/1, etc. When a PR fixes one legacy occurrence and introduces a different occurrence with the same fingerprint in the same file, the duplicate count stays unchanged, so diff() reports no new finding and the quality gate passes even though new debt was added. This affects repeated diagnostics with identical messages, including warning/error categories, so the baseline needs an additional stable per-occurrence discriminator or a location-aware duplicate match instead of comparing only the generated suffix set.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex You've correctly described the mechanics, but I'm going to push back on the fix, because the proposed remedy is information-theoretically impossible without breaking the gate's core property.

The scenario — fix one occurrence and add another with the same rule + path + normalized message** in the same file — produces two findings that are *literally indistinguishable*: same rule, same file, same normalized message, differing only in line. The **only** available per-occurrence discriminator is the line number. But the fingerprint is deliberately line-**independent**, because a debt gate that keys on line registers every finding below an inserted line as fixed+newon any PR that adds lines — i.e. ordinary code movement floods the gate with false "new debt" and it fails on essentially every PR. That churn is the single biggest failure mode of naive line-based diff gates, and avoiding it is the whole reason for the line-independent identity (it's also exactly the trade-off GitHub code scanning makes withpartialFingerprints`). A "fix-one-add-one-identical" and a "move-one" are the same event under this identity — there is no signal to separate them.

So the gate tracks the count per (rule, path, message) bucket, which is the meaningful debt measure: 3→4 in a bucket is caught as 1 new (base/3 ∉ baseline); 3→2 is caught as 1 fixed; 3→3 means the file still has the same number of the same diagnostic — actionable state unchanged, no net new debt. The swap you describe leaves the bucket count at 3; the file is no better and no worse for that diagnostic.

I'd argue that's the correct behaviour for a count ratchet, so I'm keeping the line-independent identity by default. If strict location-level ratcheting is wanted (accepting the move-churn), that's a clean --line-aware opt-in I can add later — but it shouldn't be the default. Marking 👎 on the suggested change, with thanks for the precise write-up.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_load_findings now catches OSError/ValueError/KeyError and exits 2 with a
message, symmetric with the missing-baseline path — CI gets an error, not a
traceback (CodeRabbit). Test test_cli_missing_current_fails_cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLHz4cevQyXHKZnjzCqhoa

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
report/diff_cli.py (1)

34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Chain the re-raised SystemExit to silence B904.

Ruff flags raising inside an except clause without explicit chaining. Use from None (the original message is already surfaced via the printed e) to keep the traceback clean.

♻️ Proposed tweak
     except (OSError, ValueError, KeyError) as e:
         print(f"error: cannot read findings from {path!r}: {e}", file=sys.stderr)
-        raise SystemExit(2)
+        raise SystemExit(2) from None

Source: Linters/SAST tools

report/tests/test_baseline.py (1)

126-133: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: avoid the hardcoded /tmp/x.json baseline path.

The test is correct — _load_findings fails on the missing --current before the baseline is ever read, so exit 2 is exercised as intended. The hardcoded /tmp path (flagged by Ruff S108) is harmless here since it's never opened, but using a non-existent path under tempfile.gettempdir() would silence the linter and be portable across OSes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@report/tests/test_baseline.py` around lines 126 - 133, The test in
test_cli_missing_current_fails_cleanly uses a hardcoded /tmp baseline path,
which triggers the Ruff S108 warning even though it is never opened. Update the
diff_cli.main call in this test to use a non-existent path built from
tempfile.gettempdir() instead, so the fixture remains portable and the lint
warning is avoided while still exercising the missing --current exit path.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@report/tests/test_baseline.py`:
- Around line 126-133: The test in test_cli_missing_current_fails_cleanly uses a
hardcoded /tmp baseline path, which triggers the Ruff S108 warning even though
it is never opened. Update the diff_cli.main call in this test to use a
non-existent path built from tempfile.gettempdir() instead, so the fixture
remains portable and the lint warning is avoided while still exercising the
missing --current exit path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 00eb720b-5703-4d98-87eb-80905ea4f260

📥 Commits

Reviewing files that changed from the base of the PR and between ff2b0af and e661e93.

📒 Files selected for processing (2)
  • report/diff_cli.py
  • report/tests/test_baseline.py

- diff_cli: raise SystemExit(2) from None (the original error is already in the
  printed message), silencing Ruff B904.
- test: build the non-existent paths from tempfile.gettempdir() instead of a
  hardcoded /tmp, silencing Ruff S108 and staying portable.

Both trivial; tests 8/8 (incl. -O). CodeRabbit nitpicks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLHz4cevQyXHKZnjzCqhoa

Copy link
Copy Markdown
Owner Author

@coderabbitai both nitpicks addressed in b18b453:

  1. diff_cli.py:34 (B904)raise SystemExit(2) from None (the original error is already surfaced in the printed message), traceback stays clean.
  2. test_baseline.py (S108) — the non-existent fixture paths now come from tempfile.gettempdir() instead of a hardcoded /tmp, portable and lint-clean.

Both trivial; tests 8/8 (incl. -O).


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@PhysShell: Thanks for the quick follow-up — those fixes look good. Using raise SystemExit(2) from None keeps the CLI failure path clean, and switching the fixture path to tempfile.gettempdir() makes the test portable and lint-clean. Appreciate the confirmation on 8/8 tests passing too.

🐇✅

@PhysShell
PhysShell merged commit a09b204 into main Jun 26, 2026
1 check passed
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.

2 participants