Skip to content

research: land the drift mode and the 35-repository corpus on main - #58

Merged
mevichitra merged 11 commits into
mainfrom
claude/land-research-corpus
Oct 17, 2026
Merged

mevichitra merged 11 commits into
mainfrom
claude/land-research-corpus

Conversation

@mevichitra

@mevichitra mevichitra commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

#53 (long-span drift mode) and #55 (35-repository corpus, batch runner, statistics and the committed paper/ tables) were merged into claude/nice-allen-ptg63r, the branch of #51, after #51 had already been merged into main. About 6,000 lines of research code and results never reached main. This PR merges that branch into main, resolves the conflicts with #54 (RQ2 transformations) and #56 (SonarQube, Bandit and Semgrep schemes), and fixes a bug in drift mode that only appears once the two are combined.

Type of change

  • Bug fix (false positive / false negative correction)
  • New rule (attach rule ID from the tracker)
  • Engine / runner change
  • CLI change
  • Docs only
  • Tooling / CI (research code under research/, not part of the released package)

Checklist

  • git commit -s — DCO sign-off on all commits
  • pytest passes locally
  • ruff check research/ and ruff format --check research/ clean
  • MYPYPATH=src mypy --strict research/finding_identity clean
  • News fragment: not needed for research/

What changed

Merge commit, conflicts resolved by keeping both sides:

research: score drift spans file by file: drift mode pooled every compared file's findings into one list before scoring. That was harmless for the key-based schemes, whose keys include the path. The SonarQube tracker from #56, though, compares one file's findings and lines, so the pooled input let it pair findings across files. In the drift test it "mispaired" the finding of a renamed file with one from another file. Each file pair is now scored on its own, as the consecutive mode does, and the counts are summed. The chained ground truth is unchanged.

The drift tests looped over every scheme except line, with outcomes written for key-based schemes. Those loops now cover the key-based schemes, and sonar and bandit get explicit checks:

span change sonar bandit
2 flagged line edited loses the edited finding keeps it (ignores location and code)
3 file renamed keeps the renamed file's finding (compares the file pair, not paths) loses it (compares the file name)
5 new copy of an old line takes it for the old finding (hidden) takes it for the old finding (hidden)

Test evidence

$ python -m pytest research/finding_identity/tests -q
118 passed, 1 skipped          # the skip is a live Semgrep test
$ python -m pytest -q
652 passed
$ ruff check research/ && ruff format --check research/
All checks passed!
39 files already formatted
$ MYPYPATH=src python -m mypy --strict research/finding_identity
Success: no issues found in 35 source files

Before the drift fix, the merged tree failed test_span_2_edit_loses_the_edited_finding (bandit) and test_span_3_file_rename_breaks_every_path_based_key (sonar: {'known_ok': 1, 'mispaired': 1, 'false_new': 1}).

Notes for reviewer

  • The committed paper/ tables are out of date. They come from the corpus run of 2026-10-14, which scored only line, line_text, github and the codeguard family. The paper needs a re-run with batch --force to add the sonar and bandit columns.
  • With --analyzer bandit or semgrep, the per-category totals put every finding under unknown, because the categories come from CodeGuard's rule registry. So stats has no security-only table for those runs.
  • Once this is merged, claude/nice-allen-ptg63r can be deleted.

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>
Each replay now also scores the findings of every rule category
(security, performance, quality) on their own and writes the results
to summary.json under by_category, with the same totals and collisions
blocks as the summary itself. Scheme keys include the rule ID and the
ground truth only pairs findings of the same rule, so the categories
add up exactly to the overall totals; a test checks this.

The change is additive: existing keys and outputs are unchanged.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
…dentity

corpus.csv lists 36 open-source web applications (26 Python, 10
JavaScript/TypeScript), each pinned to a commit, with its licence,
framework and why it qualifies. corpus.py pins rows to their branch
head and re-checks the measurable selection criteria at the pinned
commit from git alone: at least 200 first-parent commits in the two
years before it, a recent pinned commit, a single OSI-approved root
licence, and the declared back-end language as the majority of
analysable files. The README records the criteria and the 24 rejected
candidates with their reasons.

`batch` replays every corpus repository at its pinned SHA in parallel,
one replay process per repository. It resumes by skipping repositories
whose summary already covers the pinned SHA and commit count, and logs
failures to batch.jsonl and per-repository logs without stopping.

`stats` reports, for all findings and for security findings, pooled
rates with Wilson intervals, cluster-bootstrap intervals over
repositories, macro-averaged rates, and the median and interquartile
range of per-repository rates, as CSV and Markdown tables, with an
optional box plot (matplotlib, listed in requirements.txt).

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
The single wide Markdown table dropped the separator after the median
column. Each scope now gets a false-new table with all its intervals and
a second table for the other metrics. The box plot uses a symmetric log
axis so rates of a few percent stay visible next to the line scheme.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
…rpus

Adds paper/ with the aggregated outputs of the 35-repository run:
pooled rates with Wilson and cluster-bootstrap intervals, macro rates,
per-repository rates and spread, a per-repository Markdown table (new
per_repo.md output of stats) and the box plot of per-repository
false-new rates.

CTFd commits Vite build output under CTFd/themes/*/static/assets/ that
the harness does not skip, so its replay was dominated by generated
bundles (12,944 findings compared in its first 75 commit pairs). It is
excluded under a new, documented "no committed build output" criterion.

The README records the run's runtime and notes that renamed files make
every scheme lose its findings, the main source of the residual
false-new rate.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
…d rates

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
Add long-span drift mode with chained ground truth to the finding-identity harness
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>
Finding identity: 35-repository corpus, batch runner and statistics
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>
Drift mode pooled the findings of every compared file into one list
before scoring. That was harmless for the key-based schemes, whose
keys include the path, but the SonarQube tracker (merged into main
alongside) compares one file's findings and lines, so pooled input
let it pair findings across files: in the drift test it "mispaired"
the finding of a renamed file with one in another file.

Score each file pair on its own, as the consecutive mode does, and
sum the counts. The chained ground truth is unchanged.

The drift tests looped over every scheme but `line` with outcomes
written for key-based schemes. Restrict those loops to the key-based
schemes and check the sonar and bandit outcomes explicitly:
SonarQube keeps the renamed file's finding but loses the edited one;
Bandit, which ignores location and code, keeps the edited one but
loses the renamed file's; both take the new copy of the old line for
the old finding.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
@mevichitra
mevichitra force-pushed the claude/land-research-corpus branch from 1912a1f to 6bfe1d3 Compare October 17, 2026 05:12
@mevichitra
mevichitra merged commit 28e9a8c into main Oct 17, 2026
16 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