Skip to content

Measure how much of pr-selfcheck output survives repetition - #371

Closed
ikuwow wants to merge 9 commits into
mainfrom
measure-pr-selfcheck-run-to-run-agreement
Closed

Measure how much of pr-selfcheck output survives repetition#371
ikuwow wants to merge 9 commits into
mainfrom
measure-pr-selfcheck-run-to-run-agreement

Conversation

@ikuwow

@ikuwow ikuwow commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Purpose

/pr-selfcheck reported a defect in #367's body in one run and reported the diff fully accounted for in the next two, which is what #369 records. Nothing downstream of that observation can proceed on it: a negative from an unstable detector does not separate a rule that fails to reach a case from a run that happened to miss it, so the four Necessary wordings tested one run apiece during #366 and #367 were never actually tested, and the PASS treated as a gate on both PRs carries whatever a single draw carries.

This measures how much of the check's output survives repetition on frozen input, so that any later change to the skill has a number to move.

Key changes

  • A harness runs the check against a PR that is no longer open, records each run's report and both transcripts, and reports the verdict split, per-defect agreement across runs, and what each run had in hand
    • the third table is what decides where a fix belongs, since a run that held a source and stayed silent judged differently while a run that never held it could not have judged at all
    • grouping findings into defects is itself a model's judgement, so three passes run over the same input and two findings count as one defect when two passes agree; per-defect disagreement between passes is reported next to each row
  • Round 1 takes the merged Rule the altitude of information inside a PR body section #367 ten times and finds the verdict agrees in every run while the findings underneath it agree almost nowhere: five distinct defects, none reported by all ten runs, four reported by exactly one, the widest reaching five
    • no run reported a Fix, and only a Fix moves the verdict, so the unanimous PASS records the absence of a severity rather than agreement about the PR
  • Round 2 takes a fixture carrying Rule the altitude of information inside a PR body section #367's diff under the body that PR held at 13:54 on 2026-08-20, where the diff adds a rule to Necessary that the body names nowhere, and two of six runs report it while four return PASS
  • The runs that reach the defect are the ones that put the diff's additions and the body's claims side by side, one reading the diff whole against the base file and the other diffing base against head
    • the four that miss it verify the claims the body makes, recomputing its bullet and file counts correctly, and never go the other way to look for what the body does not mention
    • step 6 names no collection to perform before judging, and Scoped asks whether every part of the diff sits inside the stated intent, which cannot be decided without one
  • Reading the gitignored clause-mapping record does not separate the runs that reach the defect from the runs that miss it, which is the route pr-selfcheck reports different findings across runs on an unchanged PR #369 proposes as the explanation
    • one round 2 run read it and found the defect, another read it and did not
    • in round 1, reading it was independent of reporting the defect it backs: two runs read it and reported, four read it and did not, three reported without reading

What #369 asserted and what the transcripts show

The issue states the body was not edited between its three runs. The body each of those runs fetched is recoverable from its own transcript, and the three are byte-identical at 5596 characters, so the disagreement is between runs over one input rather than between two inputs.

Verification

  • Round 2's two Fix items both name the rule the body omits, quoting it from the diff and citing its absence from the base file → recorded in round2/findings.jsonl
  • The fixture's diff matches Rule the altitude of information inside a PR body section #367's → git diff --stat b9256e4 FETCH_HEAD gives the same three files and 170 insertions(+), 137 deletions(-) that gh pr view 367 reports
  • The fixture's body matches the recovered input → diff against round2/body-input.md is empty but for the trailing newline gh adds
  • The three 2026-08-20 runs fetched identical bodies → each body parsed out of its own transcript's gh pr view result, compared pairwise
  • Round 2 stopped at six runs on elapsed time, decided before any of its reports had been read, so the stopping rule is independent of what the runs found
  • The judge's three passes agree on every pair in both rounds → the judge splits column is zero throughout, so neither round's grouping rests on a single pass

Notes

Round 2's fixture carries one difference from the input it reconstructs: the recovered body cites main as the base its verification queries ran against, which named the merge base on 2026-08-20 and names a later commit now. Two of six runs reported that divergence. The defect the round measures is decided against the diff and the body alone and is unaffected; the round's other findings are not all free of it.

The fixture's branches stay on the remote because round 2 is not reproducible without them.

Follow-up work, none of it in this diff: the runs are sequential only because the check's git fetch origin pull/<n>/head races on a shared FETCH_HEAD, so a per-run clone would cut a round from roughly 40 minutes to under 10; and the collection the two successful runs performed is mechanical enough to write into the skill's steps, which is the change this baseline exists to evaluate.

ikuwow and others added 8 commits August 20, 2026 14:39
`/pr-selfcheck` reported a defect in PR #367's body in one run and reported
the diff fully accounted for in the next two, against an unedited body. A
single run is therefore not evidence, and iterating on the skill's rules by
running it once per candidate wording cannot separate a rule that fails to
reach a case from a run that happened to miss it.

The harness runs the check N times against a merged PR, whose body, diff and
head commit are frozen, so every difference between runs comes from the
checker. It records each run's report and both transcripts, and reports three
tables: the verdict split, per-defect agreement across runs, and what each run
had in hand. The third splits a miss into a run that held the evidence and
judged differently and one that never gathered it, which is what decides
whether a fix belongs in the skill's steps or in its rules.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Each run of the check reads the working tree's state, so a round whose output
accumulates as untracked files hands every run a different tree from the one
before it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Round 1 runs the check ten times against the merged #367. Its verdict is PASS
in every run, and that agreement carries nothing: no run reported a Fix, and
only a Fix moves the verdict. Underneath it the runs agree on almost nothing —
nine findings across seven runs group into five defects, no defect is reported
by all ten runs, four are reported by exactly one, and the most widely reported
reaches five.

The defect the three runs in #369 disagreed over is absent from the merged
body, which names the rule addition the 13:54 run found unaccounted for. Round
2 therefore targets a fixture carrying #367's diff under the body it held at
13:54, recovered verbatim from those runs' own transcripts. The three
transcripts hold byte-identical bodies, so the disagreement recorded in #369
is between runs over one input rather than between two inputs.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Claude Code refuses a session id it has already recorded, so a second round
numbering its runs from 1 collided on every run and produced ten empty
reports. Each round now carries an offset that keeps its ids disjoint, and
round 1's offset is zero so its recorded ids still match what produced it.

A non-zero exit was recorded as a completed run. In the tables that is
indistinguishable from a run that looked and found nothing, which is how ten
collisions passed as ten runs reporting no findings. A failed run is now
recorded as failed, excluded from the round, and taken again on restart.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The fixture carries #367's diff under the body that PR held at 13:54 on
2026-08-20, where the diff adds a rule to `Necessary` that the body names
nowhere. Two of six runs report it as a Fix; four return PASS. A defect that
blocks a PR therefore survives the check two times in three.

The runs that reached it are the ones that put the diff's additions and the
body's claims side by side -- one read the diff whole against the base file,
the other diffed base against head. The four that missed it verified the
claims the body makes, recomputing its bullet and file counts correctly, and
never went the other way to look for what the body does not mention. Step 6
names no collection to perform before judging, and `Scoped` cannot be decided
without one.

Reading the gitignored clause-mapping record does not separate them: one run
that read it found the defect and one did not, and in round 1 reading it was
independent of reporting the defect it backs.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A run refused its session id exits before the check starts and has no
transcript. The id it was refused can already name another round's session,
whose transcript was then copied into this round's directory and would be read
as this round's data. Four such files sat under round 2 carrying round 1's
runs, referenced by no record.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The check fetches the PR head into FETCH_HEAD and reads through it, which is
the only reason runs could not overlap. A clone per run removes that shared
state, so a round's wall-clock is set by its slowest run rather than by their
sum. The clone carries the gitignored paths the check reads and points origin
at the real remote, and it is taken from the main tree at the branch that tree
has checked out, so a run reaches the same rules through either path.

Sample size moves into the round's own entry, alongside the PR and the session
offset, so it is fixed before the round starts. Rounds 3 and 4 are declared
here for a variant of `Scoped`, each sharing a PR with the round it will be
read against.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`Scoped` asks that every part of the diff sit inside the stated intent without
saying what a part is. Across rounds 1 and 2 the check resolved it to the
changed-file list in 14 of 16 runs, which any body naming its changed areas
satisfies regardless of what those files hold; the two runs that resolved it
finer are the two that reached the defect round 2 plants. The unit now names
content the diff introduces, separated from moved or reformatted text by a
comparison against the base.

The preregistration fixes what counts as reaching that defect, the two counts
reported, the sample size, and the manipulation check, before either round
runs.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Round 3 ran the unit-binding variant against the same defective body round 2
used. Two of six runs reached the omitted rule, the same count round 2 reached
under the current wording, and neither raised it to a `Fix`, where round 2
raised both.

The comparison does not carry, because the fixture lets a run compare against a
`main` that already contains the change under test: #367 is merged, so its
added rule is in `main`, and the body's own verification queries cite `main` as
their base. Across rounds 2 and 3, five of twelve runs took that comparison and
none of them reached the rule, against four of the seven that did not
(one-sided Fisher p = 0.07, decided after the data rather than before it). The
recall figures from both rounds measure base selection alongside whatever else
they measure.

Two of the six runs also read the rulebook through `git show origin/main:`
rather than the working tree, so the variant never reached them. A rule variant
is testable this way only from a base the runs resolve to.

What survives is the observation that does not depend on which base a run
chose: across both rounds the check resolved `Scoped` to the changed-file list
in fourteen of sixteen runs, and the two exceptions are the two runs that
reached the omitted rule.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ikuwow

ikuwow commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Closing unmerged. #375 settled that this repository tracks configuration and not the evidence behind it, so a branch whose diff is 82 files of harness code and run transcripts has no destination here. The measurement it records stands and is cited from the tracking issues.

The branch stays on the remote, and the harness plus all three rounds are copied to ikuwowfiles/pr-selfcheck-stability/ on the authoring machine, which the global gitignore covers.

Two things a later round still needs, both still on the remote: the fixture branches fixture/367-base and fixture/367-head, and #370, the closed PR over them that round 2 ran against.

Round 2's figure is not a baseline for the next round. Six runs reaching the planted defect twice was measured against the rules as they stood on 2026-08-20, and #372 has since replaced the Scoped rule those runs resolved to a file list while #374 added a rule under Necessary. A round measuring a change to the skill's steps has to take its own baseline on the rules that are current when it runs.

The pr-guidelines.md variant this branch carries is a round 3 experiment written against the pre-#372 Scoped wording. It is superseded and is not a starting point for any rewrite.

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