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
10 changes: 6 additions & 4 deletions ANTIGRAVITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ Repeated ad hoc file drops like this (`scratch_func.py`, `pr_groovy_body.txt`, `

## CI Ruff Audit
The `ruff-audit.yml` CI job enforces a STRICT EXACT MATCH against `tests/ruff_audit_baseline.json`.
This baseline uses the line number as part of the JSON keys (e.g. `"gitgalaxy/core/detector.py:1002: PERF401"`).
**Important**: Any code edits that add or remove lines will SHIFT the line numbers of subsequent lint violations, causing the ruff audit to fail even if you didn't introduce new violations.
To fix this, ALWAYS regenerate the baseline before committing if you've added/removed lines in files with pre-existing lint violations:
`python -c "from tests.ruff_audit import run_ruff_check; import json; json.dump(run_ruff_check(), open('tests/ruff_audit_baseline.json', 'w'), indent=2, sort_keys=True)"`
Since #3384 its keys are content-based, not line numbers (e.g. `"gitgalaxy/recorders/gpu_recorder.py: C414 @2c826ccd023d#0"`:
file, rule code, a hash of the whitespace-stripped flagged line, and an occurrence index -- see `tests/lint_baseline.py`).
Adding or removing lines elsewhere in a file no longer changes the baseline, so there is nothing to regenerate for a pure line shift.
Editing a flagged line itself, or adding another identical violating line, IS a new key and fails the audit. If that's pre-existing
debt you deliberately carry (or you fixed findings), regenerate with `python tests/ruff_audit.py --write-baseline`
(`tests/mypy_audit.py` uses the same scheme and flag).
20 changes: 13 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,19 @@ language or a follow-on bug sweep rather than re-deriving the process from scrat
These are regression gates, not zero-tolerance floors: each has a committed baseline file
(`tests/ruff_audit_baseline.json` etc.) and `--ci` mode fails only on *new* findings beyond it.
This lets CI enforce "no new problems" without demanding the pre-existing backlog be fixed
first. If you fix backlog findings, regenerate the baseline by running the script without
`--ci`. `ruff format --check` is the one zero-tolerance exception (whole repo was reformatted
once at adoption, so there's no backlog to carry).
first. If you fix backlog findings, regenerate the baseline with `--write-baseline`
(`python tests/ruff_audit.py --write-baseline`, same for `mypy_audit.py`). `ruff format
--check` is the one zero-tolerance exception (whole repo was reformatted once at adoption, so there's no backlog to carry).

Use `python tests/tools/audit_check.py` (add `--regenerate`) instead of running the three
`--ci` scripts separately and manually diffing each baseline by eye — it bundles all three plus
the format check, and auto-detects "pure line-shift" findings (same file/code/message, just moved
because an earlier edit in the same file shifted everything below it) from genuine new findings
that need real review, only regenerating the former.
the format check. Since #3384 the ruff and mypy baseline keys are content-based --
`{file}: {code} @{hash of the whitespace-stripped flagged line}#{occurrence}` (`tests/lint_baseline.py`)
-- not line numbers, so an unrelated line shift elsewhere in a file changes nothing and can't
conflict on rebase. `--regenerate` only auto-accepts an *edited* baselined line (a new key that
pairs one-for-one with a now-stale entry of the same file/code/message); a new finding, or a new
duplicate of a baselined line (next `#occurrence`), still fails. Human-readable output still
prints each finding's current line number.

## Testing conventions

Expand Down Expand Up @@ -266,7 +270,9 @@ re-bless with `crucible_check.py --update`, then **verify** the resulting golden
against main is limited to this branch's own new keys (auto-detected from the pre-rebase
branch-vs-merge-base diff, or passed via `--expect-keys`) — failing loudly on any other
("foreign") drift, the same scoped-review discipline "Scoping a bless" below describes. It then
refreshes baseline entries for moved lines in touched files only and runs ruff check/format plus
runs `audit_check.py --regenerate` scoped to touched files (with #3384's content-keyed ruff/mypy
baselines, moved lines no longer need refreshing; this only re-accepts edited baselined lines) and
runs ruff check/format plus
a fast, touched-file-scoped test selection. The default is a dry run (a read-only `git
merge-tree` preview, no branch mutation); `--execute` performs it for real, and `--push` (only
valid with `--execute`) does `git push --force-with-lease` — never plain force.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ to give them), not worth the risk of touching the shared `_resolve_class_start_m
python tests/tools/tree_sitter_accuracy_audit.py --lang <x> --ci
python tests/tools/tree_sitter_accuracy_audit.py --lang <x> --regenerate # if clean/improved -- also refreshes the summary table
python tests/tools/crucible_check.py # both modes
python tests/tools/audit_check.py --regenerate # if pure line-shifts
python tests/tools/audit_check.py --regenerate # if only edited baselined lines
python -m pytest tests/core_engine/ tests/extraction/
```
If `--regenerate` refuses because of a real `extra_classes`/`found_classes` regression that
Expand Down
2 changes: 1 addition & 1 deletion tests/extraction/how_to_harden_extraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ reasonable *floor*. Push further — aim for 10-15 per major language covering,
- ReDoS scaling check if the fix touches a quantifier — `verify_candidates.py`'s
`check_redos_scaling()` for quick iteration; a real assertion (`assert_redos_immune`, already
available from `_extraction_harness.py`) once the pattern is finalized.
- `python tests/tools/audit_check.py` (add `--regenerate` if it reports pure line-shifts — see its
- `python tests/tools/audit_check.py` (add `--regenerate` if it reports only edited baselined lines — see its
own docstring) instead of running `ruff format`/`ruff_audit.py --ci`/`mypy_audit.py --ci`/
`dead_key_audit.py --ci` as four separate commands and manually eyeballing each diff.
- **Crucial Linter Step**: Run `ruff check .` to catch issues before the CI pipeline rejects them. GitGalaxy uses `flake8-bandit` which aggressively flags `assert` usage. Any `assert` you use in your test files MUST be appended with `# noqa: S101` (e.g., `assert match is not None # noqa: S101`).
Expand Down
123 changes: 123 additions & 0 deletions tests/lint_baseline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Content-keyed baseline keys shared by tests/ruff_audit.py and tests/mypy_audit.py (#3384).

WHY
Both audits used to key baseline entries as `{file}:{line}: {code}`. Line numbers
are not stable: an unrelated insertion earlier in a file shifts every finding below
it, so the baseline changed (and conflicted on rebase) even though nothing
lint-relevant changed. Rebases repeatedly had to hand-fix moved entries.

KEY SCHEME
"{file}: {code} @{hash}#{occurrence}"

- `file` repo-relative POSIX path (always first, so `key.split(":", 1)[0]`
is still the file -- rebase_rebless.py relies on that).
- `code` the rule code (ruff) / error code (mypy).
- `hash` first 12 hex chars of sha256 over the flagged source line with ALL
whitespace removed (see normalize_line), so reindenting or
re-spacing the line doesn't change it either.
- occurrence 0-based index among findings sharing the same (file, code, hash),
in source order (line, then column). Two identical violating lines
in one file get #0 and #1, so a NEW duplicate of a baselined line
produces an unbaselined #1 and is still caught; it also means two
findings of the same code on the same line no longer collapse into
one entry the way the old line-number key did.

The line number is deliberately NOT part of the key; it's carried alongside each
current finding (Finding.line) purely so human-readable output can still point at
where the finding is now.
"""

import hashlib
import json
from collections.abc import Iterable
from pathlib import Path
from typing import Callable, NamedTuple

HASH_LEN = 12


class Finding(NamedTuple):
file: str # repo-relative POSIX path
line: int # 1-based; 0 if the tool gave no location
column: int # 1-based; 0 if unknown
code: str
message: str


def normalize_line(text: str) -> str:
"""Strips ALL whitespace, so indentation/spacing-only edits don't change a key."""
return "".join(text.split())


def line_hash(text: str) -> str:
return hashlib.sha256(normalize_line(text).encode("utf-8")).hexdigest()[:HASH_LEN]


def format_key(file: str, code: str, digest: str, occurrence: int) -> str:
return f"{file}: {code} @{digest}#{occurrence}"


def parse_key(key: str) -> tuple[str, str, str, int]:
"""Inverse of format_key: returns (file, code, hash, occurrence)."""
file_part, _, rest = key.partition(": ")
code_part, _, tail = rest.rpartition(" @")
digest, _, occurrence = tail.partition("#")
return file_part, code_part, digest, int(occurrence or 0)


def source_reader(repo_root: Path) -> Callable[[str, int], str]:
"""Returns read(file, line) -> that source line's text ("" if unavailable), caching each file."""
cache: dict[str, list[str]] = {}

def read(rel_path: str, line: int) -> str:
if rel_path not in cache:
try:
cache[rel_path] = (repo_root / rel_path).read_text(encoding="utf-8", errors="replace").splitlines()
except OSError:
cache[rel_path] = []
lines = cache[rel_path]
return lines[line - 1] if 1 <= line <= len(lines) else ""

return read


def key_findings(findings: Iterable[Finding], read_line: Callable[[str, int], str]) -> dict[str, Finding]:
"""Assigns each finding its content key. Deterministic: occurrence indexes follow source order."""
ordered = sorted(findings, key=lambda f: (f.file, f.line, f.column, f.code, f.message))
seen: dict[tuple[str, str, str], int] = {}
keyed: dict[str, Finding] = {}
for finding in ordered:
digest = line_hash(read_line(finding.file, finding.line))
triple = (finding.file, finding.code, digest)
occurrence = seen.get(triple, 0)
seen[triple] = occurrence + 1
keyed[format_key(finding.file, finding.code, digest, occurrence)] = finding
return keyed


def to_baseline(keyed: dict[str, Finding]) -> dict[str, str]:
"""The committed baseline shape: {key: message} -- no line numbers, so line shifts don't touch it."""
return {key: finding.message for key, finding in keyed.items()}


def dump_baseline(baseline: dict[str, str]) -> str:
"""Canonical serialization (sorted, 2-space indent, trailing newline) -- byte-identical on regenerate."""
return json.dumps(baseline, indent=2, sort_keys=True) + "\n"


def write_baseline(path: Path, baseline: dict[str, str]) -> None:
path.write_text(dump_baseline(baseline), encoding="utf-8")


def load_baseline(path: Path) -> dict[str, str]:
if not path.exists():
return {}
with open(path, encoding="utf-8") as f:
return json.load(f)


def describe(key: str, finding: Finding) -> str:
"""Human-readable line for a CURRENT finding: current location first, stable key after."""
return f"{finding.file}:{finding.line}: {finding.code} -- {finding.message} [{key}]"
Loading
Loading