Skip to content

research: manual ground-truth check, adjudication and corrected hidden rates - #60

Merged
mevichitra merged 7 commits into
mainfrom
claude/labelling
Oct 18, 2026
Merged

mevichitra merged 7 commits into
mainfrom
claude/labelling

Conversation

@mevichitra

Copy link
Copy Markdown
Owner

Summary

Adds the manual check of the harness's ground truth (the line diff of linemap.py) that the paper's threats section needs, and the tooling behind it. A seeded, stratified sample of 100 CodeGuard findings was labelled by a human rater and, independently, by an LLM agent rater. Neither saw the harness's answer or the other's labels. The disagreements were then adjudicated. The ground truth holds for 59 of 60 persisting findings but only 30 of 40 introduced ones. Its errors sit where a scheme matched an "introduced" finding, so the paper's hidden rates are upper bounds: SonarQube's corrects from 1.47% to about 0.24%.

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

Tooling

  • gt_sample.py also writes items.json: the sampled findings shuffled under neutral ids, without the harness's answer, the stratum or any scheme's outcome. sample.csv gains the item key column. Introduced items carry the old code at the same place, found with the line map (corresponding_old_line).
  • gt_agreement.py (new) scores raters against the key, per item kind and per stratum, with Wilson intervals, and computes agreement between two raters. --adjudicated scores the final verdicts. --results estimates each content scheme's hidden rate with the ground-truth errors removed. --human-kind limits the person's labels to one kind, and --agent can be repeated so a later round overrides an earlier one.

Data and write-up (paper/ground_truth_check/)

  • The labels of both rounds, the human rater's adjudication, adjudicated.json (final verdict and source per item), agreement.md (scores), and a README.md describing the procedure.
  • The procedure includes a flaw in round 1: introduced items showed no code from before the commit, so the human rater's round-1 labels on them are not used.
  • claims.md adds G1–G6, marks the hidden rates as upper bounds, and notes that Bandit's own findings were not checked; outline.md's threats section says the same.

Results

ground truth holds Wilson 95% CI
persisting findings (pairs) 59 / 60 (98.3%) 91.1–99.7
introduced findings 30 / 40 (75.0%) 59.8–85.8
new_but_matched 10 / 20 (50.0%) 29.9–70.1
agree_new 20 / 20 83.9–100

The errors are findings that persisted while the line diff lost them: the line was edited past the similarity threshold, or moved out of a removed block. Corrected hidden rates on CodeGuard findings: sonar 1.47 → 0.24 (0.07–0.66), github 0.81 → 0.41, line_text 1.17 → 0.87, codeguard 1.87 → 1.31.

Three sampled complexity and nesting findings (F036, F065, F076) sit on an unchanged def line and both raters called them "existed". Re-running the rule on both versions shows each function was not reported before the commit pushed it over the limit, so they are new. They are recorded in adjudicated.json with the re-run as the source.

Test evidence

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

New tests cover Cohen's kappa against hand-computed values, the verdict mapping for both item kinds, loading labels from a file and from the labelling page's exported documents, per-stratum scoring, the corresponding-line lookup, and the hidden-rate correction.

Notes for reviewer

  • The LLM rater read only its blinded copy of the items, by instruction; that is not enforced by the system.
  • The 27 introduced findings the LLM rater confirmed as new had no human check, and Bandit's own findings were not sampled. Both are stated in the README and claims.md.
  • Cohen's kappa between the raters is 0.00 because the human rater confirmed every pair, so chance agreement is total. Raw agreement (59 of 60) is the meaningful figure.

Generated by Claude Code

gt_sample.py now also writes items.json: the sampled findings shuffled
under neutral ids, without the harness's answer, the stratum or the
schemes' outcomes. sample.csv's new item column is the key back to the
truth. A persisting finding becomes a pair item (same issue at the old
and the new line?), an introduced one an introduced item (new in this
commit, or existed before?). The labelling page and a second rater both
work from this file.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
gt_agreement.py reads the key (sample.csv) and each rater's labels by
blinded item id, a JSON file or the labelling page's exported documents,
and writes agreement.md: per item kind and per stratum, how often the
rater confirms the harness's truth (Wilson intervals, unclear left out),
and Cohen's kappa between two raters with the items to adjudicate.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
An LLM agent labelled the 100 blinded items of items.json as a second,
independent rater: it read only items.json, never the key or the
results. One verdict and a one-sentence reason per item. The file name
asks the human rater not to open it before labelling, so their labels
stay independent.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
An introduced item had no code from before the commit, only the diff,
and 36 items had no diff hunk near the line, so a rater could not see
whether the issue existed before. items.json now gives each introduced
item the old code at the same place, found with the ground truth's line
map: the flagged line itself when it maps back (before_mapped), else the
old code around the point where it was inserted. Ids, order and pair
items are unchanged.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
The agent relabelled the introduced items from the corrected items.json,
which now shows the old code at the same place, with instructions that
no longer assume an untouched line means the issue existed (whole-
function rules can newly fire on an unchanged line). It read only its
own blinded copy of the 40 items.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
gt_agreement.py gains --adjudicated (the final verdict per item, each
with its source) and --results (the replays the sample came from). The
report then scores the adjudicated verdicts and estimates each content
scheme's hidden rate without the ground-truth errors: every hidden
finding of those schemes lies in the new_but_matched stratum, so its
count is scaled by the share of that scheme's sampled matches the
adjudication confirms as truly new, with a Wilson range.
--human-kind limits the person's labels to one item kind, and --agent
can be repeated so a later round overrides an earlier one.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
The labels of both rounds, the human rater's confirmation of the 14
harness errors the agent flagged, the adjudicated verdicts with their
sources, the scores (agreement.md) and a README that describes the
procedure, including the first round's flaw: introduced items showed
no code from before the commit, so the human rater's round-1 labels on
them are not used.

The line diff holds for 59 of 60 persisting findings and 30 of 40
introduced ones; its errors concentrate where a scheme matched an
introduced finding (10 of 20). claims.md adds G1-G6, marks the hidden
rates as upper bounds (SonarQube's corrects from 1.47% to about 0.24%)
and notes that Bandit's own findings were not checked; outline.md's
threats section says the same.

Signed-off-by: Vichitra Attri <mevichitra@gmail.com>
@mevichitra
mevichitra merged commit c9cbc3d into main Oct 18, 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