Skip to content

Add git_numstat backend (RepoState): session-retrospective change digest - #18

Merged
onlyxItachi merged 1 commit into
mainfrom
feat/v1.2-repostate
Jul 17, 2026
Merged

Add git_numstat backend (RepoState): session-retrospective change digest#18
onlyxItachi merged 1 commit into
mainfrom
feat/v1.2-repostate

Conversation

@onlyxItachi

Copy link
Copy Markdown
Owner

Summary

TASK 3, closing the schematic: the git_numstat backend (RepoState family). New src/perfdigest/adapters/git_numstat/ (reader + mapping.py + backend.py), DOMAIN_REPO_CHANGE in core/metrics.py, one import line in server/app.py, one PROFILER_TOOLS entry ("git_numstat": "git").

Owner semantics, kept strict

  • Session-retrospective, not GitHub monitoring: binds to a SAVED git diff --numstat -M <base>.. > session.numstat file — the raw diff is the token sink, numstat is its digest. An agent reorienting after compaction reads the footprint in one summarize_report call instead of re-reading diffs.
  • Provenance axis: the usage prompt tells agents to keep snapshots next to captured perf/build/CI reports as cheap anchors pinning them to code states.
  • Facts, never verdicts: units carry paths and line counts; nothing judges footprint size. duration_us is None for every unit (a change digest has no time dimension), so summarize_report falls back to file_order — asserted — and size ranking happens by the model reading lines_added, not by a hotness heuristic we bake in.

Real-format verification (the real-report lesson, again load-bearing)

The parser grammar was written FROM real git diff --numstat -M output (git 2.53, throwaway lab repo with real commits), not from documentation. Real findings baked into reader + tests:

  • Renames have three real shapes: whole-path NOTES => TASKS.md, braced src/{parser.py => parser_v2.py} / {src => docs}/util.py, and empty-side braces docs/{ => api}/config.py (rebuilding a path over the empty side can produce //, which git's own display collapses — so we do too). Renamed units are named by their NEW path (the reorientation answer); old_path/renamed facts are in expand.
  • Git splits a rename below its similarity threshold into add+delete — observed live in the lab (a 4-line file with 5 appended lines refused to pair). Documented in the probe note and test docstring as git's real answer, not a parse bug.
  • Paths with spaces arrive verbatim; non-ASCII paths arrive C-quoted ("docs/\303\266l\303\247\303\274m.txt", core.quotePath default — a real case on any Turkish-named file) and are kept verbatim as printed: the name is honest to the artifact, we don't unquote.
  • Renames appear even WITHOUT -M on modern git (diff.renames default), but the capture command still advises -M explicitly for portability.

Headline honesty case

A binary file prints -<TAB>-: no line counts exist. lines_added/lines_deleted surface as 'not_available_in_this_export' end-to-end through get_metrics — never 0.0 — while a pure no-edit rename is a genuine measured 0.0. Both directions tested side by side (the dash and the zero must never blur).

Fixtures (all genuinely produced, none hand-written)

fixture provenance
repo_session_early_sample.numstat REAL capture on THIS repo: v1.1.1..499fb2c (27 files — the lane after the gha_log merge)
repo_session_sample.numstat REAL capture on THIS repo: v1.1.1..a82e7fd (46 files — after the previous-green merge)
repo_lab_renames_sample.numstat REAL capture from the throwaway lab repo built with real commits to exhibit every shape at once (binaries with guaranteed non-text bytes, all rename forms, space path, C-quoted Turkish filename)

Both session fixtures are paths+counts only, scanned before committing. The meta touch: the compare test reads the lane's own history through its own tool — adapters/gha_log/backend.py grew 96 → 110 added lines between the two snapshots (task 2's usage-prompt extension), delta +14.0 with delta = b - a.

Error behavior

Loud, named ValueError on: prose, JSON, the empty file (an empty diff writes an empty file — "if the session made no changes there is nothing to digest" is said out loud, not returned as a silent []), and mixed content (a numstat pasted into a bigger log would digest partially — refused loudly instead of silently dropping lines).

Test evidence

14 new tests in tests/test_git_numstat.py: registry dispatch (both formats, domain, platforms, suffixes); real session fixture parse (46 units, file order, all durations None); per-file metrics vs merged reality; summarize file-order fallback with explicit metrics arg; two-snapshot footprint evolution via compare_metrics; full lab listing (all 11 shapes); binary None honesty end-to-end; genuine-zero rename contrast; all four rename resolutions with old_path in expand; binary/printed-path facts in expand; mixed add+delete counts; three non-numstat error cases; mixed-content loudness; capture-command shape (range-based, file-bound, uncommitted variant advertised).

Full suite after rebasing onto main (which had merged cargo_diag #16 meanwhile; conflicts in metrics.py/detect.py resolved keeping both backends): 196 passed, 16 skipped (baseline 168 + 14 cargo_diag + 14 here). Ruff clean. No edits to pyproject.toml version, README.md, CLAUDE.md.

Test plan

  • uv run --extra dev pytest -q — 196 passed, 16 skipped
  • ruff check on new files — clean
  • Manual smoke: suggest_profile_command('git_numstat', 'v1.1.1') returns the range capture command with the uncommitted-only variant; platform_capabilities lists git_numstat as universally digestible
  • git remote -v verified before push (github.com/onlyxItachi/PerfDigest-MCP)

🤖 Generated with Claude Code

New adapters/git_numstat/ (reader + mapping.py + backend.py), registered via
one import line in server/app.py, plus DOMAIN_REPO_CHANGE in core/metrics.py
and a PROFILER_TOOLS entry ("git_numstat": "git") in platform/detect.py.

Owner semantics kept strict: RepoState is a token-efficient BACKWARD look at
what a session did to a repo (reorientation without re-reading raw diffs)
and the provenance axis pinning perf/build/CI reports to code states — NOT
GitHub/API monitoring, and facts only, never verdicts (no readiness
judgments baked in; ranking by lines_added is the model reading a fact).

Artifact-first: binds to a SAVED `git diff --numstat -M <base>.. >
session.numstat` file. Line grammar verified against REAL git 2.53 output
before writing the parser (the real-report lesson): renames arrive as
whole-path `old => new` OR braced `pre{old => new}post` with either brace
side possibly empty ({ => api}); empty-side rebuilds collapse the resulting
'//' exactly as git's own display does; a renamed unit is NAMED BY ITS NEW
PATH (the reorientation answer) with old_path/renamed facts in expand.
Non-ASCII paths arrive C-quoted by git and are kept verbatim (honest to the
artifact). duration_us is None for every unit — a change digest has no time
dimension — so summarize_report falls back to file order, asserted.

Headline honesty case, end-to-end through get_metrics: a BINARY file prints
'-<TAB>-' — no line counts exist, so lines_added/deleted surface as
'not_available_in_this_export', never 0.0 — while a pure no-edit rename is
a genuine measured 0.0. Also observed and documented: git splits a rename
into add+delete below its similarity threshold; that split is git's real
answer, not a parse bug (probe note + test docstring).

Fixtures, both genuinely produced: (1) repo_session_early/…_sample.numstat —
real captures on THIS repo of v1.1.1..499fb2c and v1.1.1..a82e7fd (the v1.2
lane's own footprint; paths+counts only, scanned); the compare test shows
gha_log/backend.py growing 96->110 added lines between the two merges — the
session's own history read back through the tool. (2)
repo_lab_renames_sample.numstat — real capture from a throwaway lab repo
with real commits engineered to exhibit every numstat shape at once
(binaries, all rename forms, space path, C-quoted Turkish filename).

Loud ValueError on non-numstat input including the empty-diff-empty-file
case, and on MIXED content (numstat pasted into a bigger log) — partial
digests never happen silently. 14 new tests; full suite 182 passed + 16
skipped (baseline 168 + 16).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@onlyxItachi
onlyxItachi merged commit f488184 into main Jul 17, 2026
4 checks 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.

1 participant