Skip to content

fix(002): parse findings at emit time — review counts can no longer be blind - #19

Merged
ashbrener merged 8 commits into
mainfrom
fix/emit-time-finding-parse
Aug 15, 2026
Merged

fix(002): parse findings at emit time — review counts can no longer be blind#19
ashbrener merged 8 commits into
mainfrom
fix/emit-time-finding-parse

Conversation

@ashbrener

Copy link
Copy Markdown
Owner

The bug, found in production use

Wingman wrote findings: [] on reviews where the reviewer had actually reported findings in prose. The parse happened somewhere downstream of the artifact, so the artifact recorded zero and every consumer believed it.

It was caught live during a PP arc session: a session reported "latest backend review: 0 findings" from the artifact, and the migration backfill then recovered 16 findings across 5 branches that had been reported and never counted. In one repo the visible finding count went from a handful to 56.

That is the worst failure mode for a review tool — not missing a problem, but reporting confidence it hasn't earned.

What this does

  • Parses findings at emit time into an additive parsed_findings field (schema v4), so the artifact reflects what the reviewer actually said at the moment it said it.
  • Counts take MAX of both sources, so a legacy artifact can never read lower than reality.
  • One anchored parser, single-source, with a fixture verifier — the fixtures cover the shapes that broke it: prose containing pipes, diff-quoted lines, legacy priority formats, incident-native output, and genuinely empty reviews.
  • Backfill migration (scripts/migrate-reviews.py) recovers findings from v1 artifacts, idempotently.
  • Two follow-on fixes from the first real runs: diff markers are not filenames (false positive), and _convergence.json is a ledger, never an artifact to migrate.

Lifecycle

Full Spec-Kit pack: spec → plan → tasks → implementation, tests-first (fixtures and verifier red before the parser landed). 36 files, +4,866.

Known issue worth a follow-up

Running the migration against an already-v4 tree, --dry-run reports files as "would migrate" while the real run correctly reports them "unchanged" — observed on a 38-file tree. Dry-run and apply disagree, which undermines exactly the confidence dry-run exists to provide. Not fixed here; raising it rather than letting it sit unnoticed.

Ash Brener and others added 8 commits August 15, 2026 22:24
…hat the reviewer found

Live incident, four arcs: a codex review reporting three real findings
(two high, incl. an audit race emitting stale prior values) emitted
findings:[] with p1/p2 counts 0, stop_rule_met true, and a notice
recommending the author declare convergence. Root cause: the reviewer
became pluggable and codex kept its NATIVE line shape, while the
round-tracking counter still recognises only the legacy [P1] contract
used by the constructed gemini/claude prompts — so every codex review
counts zero and the stop-rule fires on round one.

A signal that cannot fail is worse than none: silence invites a look,
'converged' does not.
…ored single-source parser + fixture verifier
The artifact now carries parsed_findings (machine-parsed from the
reviewer's native line shape) alongside an untouched findings:[] — so a
reader asking 'what did the reviewer find' has an answer, and an empty
findings[] can never be mistaken for a clean review. Priority counts are
max(native parse, legacy grep), so neither supported shape can zero the
other and no single format change can blind the signal again.

Two blindnesses fixed, not one: the native (codex) shape was never
parsed, AND the legacy counter never matched '- [P1]' bullet-prefixed
lines — which is the exact format that made project-arc's own rounds
report zero. Replaying tonight's artifacts through the fix: reviews that
reported 'stop-rule met, declare convergence' actually carried up to
4 P1 + 14 P2.

Verified by scripts/verify-parser.py, which extracts the parser from the
hook between unique anchors (single-sourced — no second copy to drift)
and runs four recorded fixtures: the live incident (3 findings), the
legacy shape, an honest NO FINDINGS, and prose-with-pipes as the
false-positive guard. Wired into CI. Schema 3→4 with a migration that
keeps older artifacts readable; hook marker 4→5 so installed copies
upgrade via /review-setup.
…e first real run

The end-to-end run of this very change parsed three findings out of its
own diff: a reviewer quoting a hunk of a file that contains
finding-shaped lines ('+path | logic | high | ...') manufactured
findings. The file component must look like a path — a markdown list
marker is allowed, a diff marker never is. Regression fixture
tests/fixtures/diff-quoted.txt covers additions, removals, and a
line-less form.
…er counted

The reviewer's output was captured correctly all along; only the parsing
was blind. So the upgrade path re-READS what is already on disk rather
than stubbing an empty array: migrate-reviews.py locates the installed
hook (honouring core.hooksPath, falling back to the cwd's repo),
extracts the anchored parser from it — single-sourced, so backfill can
never drift from the emitter — parses each artifact's raw_review, and
recomputes the counts. A round with findings has stop_rule_met forced
false, retracting every 'declare convergence' the blind counter emitted.

Every other field is preserved byte-for-byte (raw_review, findings[],
resolutions, ci_status). Idempotent: a second run reports 0 migrated.
--dry-run shows what would be recovered before writing. Verified on
copies of live artifacts from two repos: 3 findings recovered from one,
a false stop_rule_met corrected on the other.

No re-review needed anywhere — re-running codex would cost hours and be
LESS accurate, since several affected branches have since merged.
…edger

The *.json glob swept the round-tracking ledger and reshaped it into a
review artifact, destroying its rounds[] history — done to a live ledger
before the guard existed. Skip underscore-prefixed files by name AND
require raw_review by shape, so any future internal state file is safe
by default. Verified: a ledger-shaped file passes through untouched
while the artifact beside it still migrates.
@ashbrener
ashbrener merged commit 10cc68a into main Aug 15, 2026
3 checks passed
@ashbrener
ashbrener deleted the fix/emit-time-finding-parse branch August 15, 2026 20:54
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