Repository navigation
Add long-span drift mode with chained ground truth to the finding-identity harness - #53
Merged
Conversation
Consecutive commit pairs are too easy a test: almost every persisting finding has only moved. Baselines are created once and compared with much later versions, so add a "drift" mode that picks an anchor commit c0 and scores every identity scheme between c0 and c_k for each span k. The ground truth is chained: each anchor finding's line is followed through every intermediate first-parent commit with map_lines on that step's diff, following renames per step, and records whether any step edited it. Only c0 and each c_k are scanned; intermediate commits are read only as blobs of files that hold tracked findings. - drift.py: trace_files, chain_lines, per-span comparison, CLI arguments - summarize.py: per-span tables, scheme x span pivots, pooled "ALL" runs and a long-format CSV (label, span, scheme, rates) for plotting - matching.py: split the scoring out of evaluate() as score(), unchanged behaviour, so both modes share the outcome taxonomy - gitrepo.py: files() lists a commit's tree - cli.py: "drift" and "drift-summary" subcommands - tests: synthetic repository with insertion, line edit, file rename and function rename; chaining and lineage unit tests Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
mevichitra
added a commit
that referenced
this pull request
Oct 14, 2026
Brings in the long-span drift mode (PR #53). The conflicts were in the command line and the README, where both sides only added: cli.py registers the drift, drift-summary, batch, corpus and stats commands, and the README keeps both the corpus sections and the drift-mode limitations. Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
5 of 11 tasks
mevichitra
added a commit
that referenced
this pull request
Oct 17, 2026
PRs #53 (long-span drift mode) and #55 (corpus, batch runner, statistics and the paper/ tables) were merged into the branch of #51 after #51 itself had been merged, so they never reached main. Conflicts, all resolved by keeping both sides: - cli.py: register the transform subcommand (main) next to drift, drift-summary and the corpus commands; replay keeps main's --analyzer and per-analyzer schemes. - scan.py: the Scanner keeps main's pluggable analyzer and gains the per-category totals. - requirements.txt: main's pinned research dependencies; matplotlib now also serves `stats --plot`. - .gitignore: ignore tables/ as well as fidelity/.build/ and .cache/. - README.md: keep both the Fidelity/RQ2 sections and the Corpus/Batch/Statistics sections; note that the committed paper/ tables predate the sonar and bandit schemes. Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a long-span ("drift") mode to the finding-identity harness (
research/finding_identity/). Consecutive commit pairs turned out to be too easy: in the pilot, 690 of 695 persisting findings had only moved. Real baselines are created once and compared with much later versions.python -m research.finding_identity drift <repo> --anchor-back 200 --spans 1,10,50,100,200picks an anchor commit c0 and scores every identity scheme between c0 and c_k for each span k. It uses the same outcome taxonomy and metrics as the consecutive mode, reported per span.Chained ground truth. The harness follows each anchor finding's line through every intermediate first-parent commit using
linemap.map_lineson that step's diff, and follows renames per step (git diff -M). It does not use a direct c0→c_k diff. A finding persists when its chain is unbroken and a finding with the same rule sits at the mapped line in c_k. It counts as edited if any step edited the line. Only c0 and the c_k are scanned. Intermediate commits are read only as blobs (viagit cat-file --batch) of files holding tracked findings.Based on
claude/nice-allen-ptg63r(#51, not yet merged), so this PR targets that branch.Type of change
src/)Checklist
git commit -s— DCO sign-off on all commitspytestpasses locally (629 passed), pluspython -m pytest research/finding_identity/tests(20 passed)ruff check research/andruff format --check research/cleanMYPYPATH=src mypy --strict research/finding_identitycleanWhat changed
drift.py(new):trace_files(file lineage and first change per anchor file),chain_lines(line chains with renames and an edited flag), per-span comparison, and thedriftCLI arguments.summarize.py: additive. Adds one table per span, scheme × span pivots for false-new and hidden rate, a pooled "ALL" table per span, and a long-format CSV (label, span, scheme, …rates) for plotting. The existing functions are unchanged.matching.py: the scoring part ofevaluate()is now a separatescore()function, so both modes share the outcome taxonomy.evaluate()behaves exactly as before.gitrepo.py: addsfiles(commit)(ls-tree).cli.py: adds thedriftanddrift-summarysubcommands. Existing code is not restructured, andschemes.pyandscan.pyare untouched.README.md: documents the mode, the chained ground truth, what gets compared, the cost, and the limits.Test evidence
The synthetic repository has five commits after the anchor: lines inserted above both findings, an edit of the flagged
evalline,lib.py→pkg/lib.py, a rename of the function around theexecline, and a new function whose line equals the anchorevalline's original text. For each span the tests assert the chained truth (persisting/edited/introduced, renamed files) and each scheme's outcome:lineloses findings on the insertion.line_textandcg_no_scopehide the new finding.githublosesexeconce code is appended after it.The unit tests also show that the chain follows a three-step gradual rewrite that a direct v0→v2 line diff leaves unmapped.
Results: anchor 200 first-parent commits back
Ran with categories
security,performance,quality. Results are in the git-ignoredresearch/finding_identity/results/drift/and are not committed.maincd1b68c(2023-04-25)d086db8(2026-10-07)dev08db92cd(2026-09-03)559a677f(2026-10-14)Pooled (both repositories), by span
Per-span tables: pallets/flask
Per-span tables: DefectDojo/django-DefectDojo (
--ref dev)Notes for reviewer
codeguard,line_textandcg_no_mask/cg_rawlose none.githubloses 2.4% at k=200, because its hash reads ~100 characters after the line, so changes below a finding break it.lineloses 50–100%.cg_no_scopeon flask at k=200 (31.6% false-new, 46.4% hidden) comes from key collisions, not from a ground-truth error. Two test files hold 12CG-PERF-004findings each whose statements normalize to the same key (73.6% collision rate). The scheme pairs equal keys in line order, so when new copies are added, the truly persisting findings go unmatched and the new ones are matched to old ones.false_knownbehind the 25% / 3.6% hidden rate on flask issrc/flask/blueprints.py:19(Blueprint.__init__, CG-QUAL-005). The first-parent step that merged the sansio split (1d8b53f) moved the old class tosansio/blueprints.pyand wrote a new subclass at the old path. In one step git sees a modification ofblueprints.py, not a rename, so the truth calls the new__init__introduced, while every key built from the line text (all schemes exceptlineandgithub) matches it to the old one. Both readings are defensible. This is the documented "code moved to another file" limit of a line-diff ground truth.renamed_files= 0). The rename path is covered by the synthetic test.