Skip to content

review: the re-review mode dial (full | scoped | flip-gated | fast)#246

Merged
jwbron merged 75 commits into
mainfrom
jwbron/rereview-mode-dial
Jul 13, 2026
Merged

review: the re-review mode dial (full | scoped | flip-gated | fast)#246
jwbron merged 75 commits into
mainfrom
jwbron/rereview-mode-dial

Conversation

@jwbron

@jwbron jwbron commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

The re-review mode dial: the runs-per-PR cost lever from the plan of record's "Re-review economics" section. Stacked on fold-in batch two (#244 -> #245 -> #247, base jwbron/review-dispatch-tax); the eval stack (#232-#238, #243) merged to main 2026-07-10.

Why

Two measured lifecycles of the same seeded PR paid the full 11-agent fan-out on every push, with per-run cost rising as threads accumulate (v1.3.1: $7.19 / $8.77 / $9.14; v1.4.0: $8.31 / $9.87 / $11.29, runs 29044383251 / 29045581242 / 29046928431 in Khan/webapp). The approval run is the most expensive while emitting the least. Both lifecycles also caught a fresh seeded defect on a re-review push that a reconcile-only fast path would have missed, which argues for scoped as the default reduced depth.

What

A per-repo mode line in .github/aw/review/ROUTING:

re-review full|scoped|flip-gated|fast
  • full: today's behavior, and the default. No consumer changes behavior until its repo sets a mode line.
  • scoped: full roster, staged only the hunks new since the last fully-reviewed fingerprint; comments scoped to those hunks.
  • flip-gated: reconcile plus the correctness pass over new hunks; a REQUEST_CHANGES to APPROVE flip is vetoed by that pass's validated blocking findings (the findings gate the flip instead of being discarded).
  • fast: reconcile-only.

Three guards, all deterministic (workflows/review/lib/rereview-mode.ts):

  1. Ready-for-review anchor: the one full review the cheap modes lean on is taken at ready-for-review, never on a draft skeleton (a draft-taken fingerprint forces one more full review when the PR leaves draft).
  2. Flip gate: as above; mechanically this rides the existing verdict function, since a validated blocking finding posts and blocks.
  3. Divergence tripwire: every full-depth review stamps a content-hashed hunk signature into its review body as a hidden comment, so it survives cache eviction. Each push is compared against the last fully-reviewed fingerprint; at unreviewed share >= 0.4 the run re-arms to full. Signatures are computed over the generated-stripped diff, so lockfile churn neither enters the fingerprint nor counts as divergence.

Named interactions handled by construction:

  • dismiss-stale-approvals: the prior verdict and fingerprint live in the stamp, not the review state, so a dismissed approval still anchors and still reads as an approval.
  • COMMENTED-only prior review: the stamp itself (not the verdict state) marks "a full review happened", so a comment-only history reduces normally after its first stamped run instead of re-running full forever.

Eval and measurement

The roster-level evals live in #251 (stacked on this PR): open-PR corpus cases with reconciler dispatch and flip-gate/duplicate scoring, the per-mode pricing sweep (eval/rereview-sweep.ts), and the ported seeded lifecycle. This PR carries the deterministic layer:

  • Adversarial lifecycle cases as data (eval/lifecycle/): rewrite-after-approval and sparse-pr-then-payload, each scored as "the tripwire re-armed and the payload got a full review", plus an economy control (fix-push-stays-cheap) scored as "the reduced path actually ran". Replayed deterministically in eval/lifecycle.test.ts under all three reduced modes.
  • Pricing wiring for the live A/B: each run's executed depth lands in out/rereview-plan.json; lib/counters.ts aggregates costByRereviewDepth; the review-trial skill gains a ROUTING-override arms pattern (two arms at the same ref differing only in the mode line) and scores adversarial lifecycle pushes.
  • This is the playbook's cost-affecting class: mode changes are earned per repo through the live A/B on recall and dollars, the way enable lines are.

Composition with batch two

Based on the top of the batch-two stack (#243 -> #244 -> #245 -> #247) so the mode dial's re-review paths compose with the re-review accountability rendering (#244) rather than race it: the accountability section stays the visible body surface; the fingerprint stamp is appended last as a hidden comment; and the flip gate reads keptBlockingCount, a small additive field this PR adds to the accountability renderer's rereview.json, so "did the reconciler keep any blocking thread" is a number read at verdict time, not a label judgment re-made there. The orchestrator wiring (Step 1 prior-review staging, the Step 3 depth plan and per-depth dispatch/staging, the Step 4 flip rule, the Step 6 stamp, reduced-depth skips in Steps 7/8) is the last commit on the branch.

Supersedes

Khan/frontend#13420 (frontend's consumer-side re-review stopgap, held on the old pin) is superseded by this: the dial lives in the shared workflow and frontend opts in with a single re-review line in its ROUTING file once priced. Frontend is deliberately not touched here.

Follow-up: staging moves pre-workflow (2026-07-13)

Per review feedback (jeresig): the deterministic staging this PR wires through orchestrator prompt steps (the Step 1 prior-review fetch, the rereview-mode CLI invocation, the scoped-diff swap) needs no model output and will migrate into pre-workflow steps the agent just reads, folded into the round-three deterministic-orchestrator work rather than special-cased here. The eval producer moves with it so the A/B keeps measuring production. This PR's prompt wiring is interim and gets deleted wholesale when that migration lands.

jwbron added 22 commits July 9, 2026 12:47
…live cases

Phase 1 of the live A/B eval plan (#232). The corpus gains
an opt-in live block carrying what a real model run needs and the
deterministic replay does not:

- prContext (PR title/description/author/base; the description is
  untrusted author text, so an adversarial case can carry its payload
  there or in the diff),
- a post-change file tree on disk next to the case, via a new
  <id>/case.json + <id>/tree/ layout that coexists with flat <id>.json
  (a directory containing case.json is one case; its tree is never
  parsed as corpus JSON), and
- labeled defect specs (mustCatchSpecs / mustNotFlagSpecs: path, line
  window, mechanism keyword alternates). Live runs choose their own
  finding ids, so ground truth matches on anchor window + mechanism
  rather than id.

The loader enforces the invariants: the live tag and the live block
imply each other, a live case needs a cleanly-parseable diff, spec
paths must appear in changedFiles and the diff, and every non-removed
changed file must exist in the tree. loadLiveCorpus() returns the
subset. The live half lives in corpus/live.ts; loader.ts re-exports it
as the single public surface.

Ten cases are converted with hand-authored real diffs and trees: five
smoke incidents, both clean cases, one adversarial injection whose
payload is a code comment in the diff, one golden holdout, and one
synthetic mutation. Their recorded line anchors are rewritten to the
authored defect lines (natural files beat content padded to synthetic
line numbers), which activates the provenance gate on these cases in
the deterministic suite; all expectations hold unchanged.
…action and case staging

Phases 2a/2b of the live A/B eval plan (#232), stacked on the Phase 1
corpus format (#233).

agent-extract.ts turns review.md's '## agent:' sections into data
(name, description, pinned model, prompt body). It takes the markdown
as a string with no fs access, because the baseline arm of an A/B
reads the merge-base review.md via git show. Parsing is strict; a
malformed or model-less section throws listing every problem, since a
silently dropped agent would skew an eval arm without failing it. An
integration test extracts the real review.md (21 agents) and pins the
staging-root reference so a future rename fails a test instead of
silently staging nothing.

live-stage.ts materializes the production staging layout for one
live-enabled corpus case: pr-context.json (review.md Step 1's shape,
synthetic identity fields), full.diff / pr.diff / full-stripped.diff
(all the case diff; corpus diffs carry no generated files and no
pattern-triage pass runs), files.json + review-files.json with the
hasPatch cross-check derived from the diff parse, provenance.json,
routing.json from the deterministic router, an out/ directory, and
the post-change checkout copied from the case tree.
rewriteAgentPrompt swaps the production staging root for the staged
context dir. Everything sits behind an injected-fs seam and is
memfs-tested.

Model dispatch (phase 2c) is deliberately absent; it needs the Agent
SDK dependency decision and arrives separately.
… runner (phase 2c)

live-producer.ts runs the live roster over one staged case behind an
injected LiveAgentRunner seam (the judge.ts pattern), so its logic is
stub-tested with zero model calls. Roster: the default finders
(correctness-reviewer, skill-auditor) plus the router's lensesToSpawn;
no pattern-triage or thread-reconciler in eval. It maps all three
sub-agent output contracts into the shapes the deterministic runner
consumes: label-shape findings (correctness lens, or conventions for
the skill-auditor so labelForFinding reproduces the best-practice
variants; confidence defaulted to 0.7 per the production claims rule),
structured-schema lens findings validated as-is, and the validator's
{claims: [...]} verdicts into CaseVerification[]. It stages
claims.json for the validator, resolves {{#runtime-import}} directives
against the case tree (a case opts into a skills index by carrying the
file), retries once on malformed output with the rejection fed back,
keeps partial results when an agent fails twice, prefixes colliding
finding ids, and reports per-agent cost/turns/wall-clock.

live-runner.ts is the one module that touches a real model runtime:
an Agent SDK query() per dispatch with Read/Grep/Glob only, cwd pinned
to the staged checkout, the agent's pinned model, and hard turn and
wall-clock caps; plus the CLI smoke entry
(tsx live-runner.ts --case <id>, requires ANTHROPIC_API_KEY). The
investigation-cap CLI the prompts reference is not staged; its own
denied-budget fallback applies. Adds @anthropic-ai/claude-agent-sdk
as a dev dependency.
live-match.ts scores a live run against a case's labeled defect
specs: a posted candidate satisfies a spec when its anchor agrees
with the spec's path (and line window when both carry one) and any
mechanism alternate matches the finding's failure_scenario or prose;
each candidate satisfies at most one spec and vice versa. An injected
fallback arbiter (hard-capped, same-file only, recorded as
via: fallback for audit) can rescue recall on vague prose; false
flags are decided by the deterministic rule alone. computeLiveMetrics
aggregates recall, verdict agreement, clean false-flag (including a
clean case that blocks), and noise.

live-ab.ts is the arm orchestrator and CLI: baseline review.md from
git show <merge-base>, candidate from the working tree, both arms
over the same live corpus with everything else (corpus, lib, runner,
metrics, judge) from the candidate, per the plan's settled decision
to isolate the model seam. Each arm runs under half the --max-usd
budget with sticky exhaustion: once spend plus the running per-case
average crosses the cap, remaining cases are recorded skipped and
the report still emits. Spec-level regressions are diffed only over
cases both arms scored. Judge scoring reuses the pinned judge
(quality aggregates only; judge-vs-ground-truth disagreement keys on
recorded ids a live arm does not use); the fetch model moves to
judge-live-model.ts and live-judge.ts now imports it. runner.ts
gains an optional RunOptions.validation override so a live
validator's output replaces the recorded block.

Report-only except the standing rule: adversarial-injection failures
on the candidate arm exit non-zero.
Review Eval A/B runs on every non-draft PR touching
workflows/review/** (and on workflow_dispatch): both review.md arms
over the live corpus via live-ab.ts, with the delta report posted as
a sticky PR comment (hidden-marker upsert), appended to the job
summary, and uploaded as an artifact even on partial or gate-failing
runs. Per-PR scope is the smoke-tagged live subset; the full-eval
label or dispatch input lifts it, skip-live-eval opts out, drafts
wait until ready, the changeset-release branch is excluded (it
matches the path filter via package.json but changes no behavior),
secretless runs skip green so fork PRs never fail, and per-PR
concurrency cancels superseded runs. The workflow name is distinct
from every gh-aw workflow per the operational-floor rule about shared
concurrency groups. live-ab.ts gains --smoke-only and writes
out/live-ab-report.md alongside the JSON for the comment step.
… and vitest

Tree directories are byte-exact case fixtures paired with each case's
diff: prettier auto-formatting one would silently desync it from the
diff the provenance gate parses, and a tree may carry a *.test.ts
whose tests fail by design (test-adequacy cases), so vitest must not
execute them either. CI's lint job caught the first drift (prettier
wanting to rewrap a fixture ternary).
…rpus' into jwbron/live-eval-producer-staging
…s with the case id

Live agents choose their own finding ids, so every case's first
correctness finding was live-correctness-reviewer-1; ids were unique
within a case but collided across cases, and judge.ts's score join
requires arm-global uniqueness. Caught by the first real A/B run
(both arms completed, then judge aggregation threw). Ids are now
<caseId>:<id> from the moment of parsing, so claims.json, the
validator round-trip, the matcher, and the judge all see the same
namespaced id.
…eport instead of killing it

The first real A/B run spent both arms' budgets and then died in
judge aggregation, writing no report: the exact
everything-spent-nothing-posted failure mode the plan forbids. Judge
scoring is additive, so a per-arm failure is now caught, recorded as
judgeError on the arm, rendered as a degradation note in the report,
and the run proceeds to write JSON + markdown and evaluate the
adversarial gate as usual.
…A/B phase 5)

Packages the seeded-defect live-trial pattern (Khan/webapp#40678) as
a Claude Code skill so a trial costs an operator an afternoon instead
of a week of hand choreography. The skill collects required inputs
(seeded branch, human-authored defect table, arms, budget approval)
and refuses to improvise ground truth; sets up one isolated PR per
arm with per-arm trigger recipes and the distinct-workflow-name rule
(same-named gh-aw workflows share a per-PR concurrency group and
cancel each other); collects reviews, artifacts, and costs per run
with the known gh-aw artifact-bug tolerance; drives optional
lifecycle pushes; scores defect by defect with the deterministic
rule mirroring eval/live-match.ts plus audited manual judgment;
exports live-enabled corpus case skeletons with a sanitization gate
for private-repo content landing in this public repo; and cleans up
trial PRs, branches, and temporary workflows. Trials remain the
architecture-bet instrument; per-change evals belong to the corpus
A/B.

.gitignore narrows from .claude to .claude/* with a !.claude/skills/
carve-out: local agent state (settings, worktrees) stays untracked,
project skills are shared tooling and are committed.
…ity, code-rendered from the reconciler keep-list
…oad (allowed-paths must match staging-relative paths)
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9b047d5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
review Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jwbron jwbron force-pushed the jwbron/rereview-mode-dial branch from be4202d to 6cee4a1 Compare July 9, 2026 22:11
@jwbron jwbron changed the base branch from jwbron/review-out-of-lane to jwbron/review-dispatch-tax July 9, 2026 22:11
@jwbron jwbron marked this pull request as ready for review July 9, 2026 22:16
@khan-actions-bot khan-actions-bot requested review from a team and jeresig and removed request for a team July 9, 2026 22:17
@github-actions

This comment has been minimized.

input.currentSignature,
input.priorStamp.anchorHunks,
);
if (divergence.unreviewedShare >= threshold) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): The inclusive >= boundary is the whole point of the tripwire, but share === 0.4 exactly is never tested — the decideReReviewDepth cases cover 1.0/0.5 above and 1/6, 1/3 below the wire. A refactor to > would slip a diff sitting precisely on the threshold through as a cheap re-review with no test failing. Add a case with 2 of 5 unreviewed hunks (share 0.4) asserting depth === 'full' and tripwireRearmed === true.

Low-confidence (1)
  • workflows/review/lib/counters.ts:291costByRereviewDepth: a depth group mixing cost-bearing and cost-less runs divides usdSum by all runs, understating per-run cost; only uniform groups are tested (may be intentional, mirroring tokensPerRun).

* `full` is the default everywhere: a repo pays for a cheaper mode only by
* writing a `re-review` line in its ROUTING file.
*/
export const RE_REVIEW_MODES = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought (non-blocking): The changeset's own evidence ("a fresh seeded defect was caught on a re-review push that a reconcile-only path would have missed; the case for scoped as the recommended reduced depth") argues for full|scoped, yet fast and flip-gated — both reconcile-only for fresh code — ship now as permanent surface before any consumer has priced even scoped. Consider shipping full|scoped and adding the reconcile-only modes in a follow-up if the live A/B shows scoped doesn't capture enough of the savings. (Counter-argument acknowledged: the A/B needs the modes to exist to price them.)

* pushed onto a sparse PR (share near 1.0) always re-arms. Exported so the
* eval suite and the live A/B can price other settings.
*/
export const DEFAULT_TRIPWIRE_THRESHOLD = 0.4;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought (non-blocking): computeDivergence uses an unweighted hunk-count share — no weighting by changed lines or ROUTING risk tier — so under fast a single hunk pushed onto a 6-hunk reviewed PR sits at ~0.17 and never trips the 0.4 wire, getting zero finding-producing review. Both adversarial lifecycle cases use payloads that dominate the count, so this small-payload weak spot isn't exercised. A floor rule (any unreviewed hunk in a high-tier path re-arms) or a line-weighted share would close it. Not a regression today since full is the default.

a category call. Count the blocking labels in your final comment set; zero blocking
labels means APPROVE.

**The re-review flip rule (reduced depths only).** One addition to the rule above

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (non-blocking): The flip gate is computed as data (flipGate in the plan, keptBlockingCount in rereview.json), but I don't see any code comparing the submitted verdict against it — computeVerdict keys only off posted labels, and the veto lives entirely in this Step 4 prose. Is a code chokepoint (e.g. the stamp CLI refusing an APPROVE stamp when depth is flip-gated/fast, the prior stamp verdict was REQUEST_CHANGES, and keptBlockingCount > 0) intended as a follow-up, or is prompt-following the deliberate enforcement for this guard?

Comment thread workflows/review/lib/rereview-mode.ts Outdated
# Conflicts:
#	workflows/review/eval/corpus/live.ts
* world (its cwd, with no network), so the agent WILL often try to read
* an imported module or caller that is not there; that read returns an
* ordinary not-found tool error the agent tolerates and works around,
* not a run failure. The cost of a missing file is realism, not a crash:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought (non-blocking): The "not a run failure" guarantee for a missing tolerated read is enforced elsewhere (live-runner.ts absorbs per-tool read errors and throws only on run-level subtype !== "success"), and no test near this comment pins it. The corpus-side promise can silently drift from the harness if the staging/runner behavior later tightens. Consider cross-referencing the runner or adding a small assertion so the two stay coupled.

@jeresig jeresig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Pushing back a bit so that we move more of this staging logic into a pre-workflow step, rather than relying on the agent to run this.

RIGHT-side line numbers of the added lines inside in-scope hunks. Step 3 uses this to
filter candidate comments.

**Stage the bot's prior reviews.** Fetch the PR's reviews (`pull_requests`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Musing: I think we would save a lot of money/tokens/time by moving a lot of this (and similar staging) logic into Github action steps that run before the workflow that downloads and stages everything and then it can just read what it needs, if it needs it.

**Decide the re-review depth (deterministic code).** After the provenance CLI, run
the re-review mode CLI from the shared lib checkout, once:
```
cd gh-aw-review-lib && npx -y tsx workflows/review/lib/rereview-mode.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we did the pre-run we could run these scripts ourselves and then just feed in the results in here (which would also improve speed and reduce token usage).

```
cd gh-aw-review-lib && npx -y tsx workflows/review/lib/rereview-mode.ts
```
It reads `routing.json` (the repo's `re-review` mode line, default `full`),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some of this context isn't needed, the reviewer here only cares about what the file is, not how it arrived at that solution.

Comment on lines +559 to +563
- **`depth: scoped`**: the full roster runs, but over only the unseen hunks. Before
Phase 1, overwrite `/tmp/gh-aw/review/full-stripped.diff` with the contents of
`scoped.diff`, and in Phase 1 build `pr.diff` from the `scoped.diff` sections of
the triage `reviewFiles` (a `reviewFiles` entry absent from `scoped.diff` is
already reviewed; leave it out of `pr.diff`). Everything else, the provenance

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this feels like stuff that we could just set up ahead of time so that it can't ever get it wrong.

@jwbron

jwbron commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Agreed, and this is where round three is headed: the deterministic-orchestrator work covers exactly this. The staging layer you flagged (the prior-reviews fetch, the rereview-mode CLI run, the scoped-diff swap, plus the existing provenance and new-scope steps that share the pattern) needs no model output, so it can all run as pre-workflow steps the agent just reads. Rather than special-casing this PR, I'll fold it into that migration so the whole layer moves at once and the eval producer runs the same pre-steps (otherwise the A/B stops measuring what production does). Merging this as-is since the dial defaults to full and follows the existing CLI-in-prompt pattern; the prompt mechanics here get deleted wholesale when the staging migration lands.

@github-actions

Copy link
Copy Markdown
Contributor

Review Guidance

github-actions (6 files)
File Reason
rereview-mode.ts Deterministic layer that decides how deep every repeat review runs on every consumer PR; a wrong operator or share miscount could silently skip review of fresh code.
rereview.ts Adds keptBlockingCount, the number the flip gate reads to veto a REQUEST_CHANGES→APPROVE flip.
routing-config.ts Parses the new re-review ROUTING line on every consumer; malformed input must degrade toward more review, never less.
router.ts Threads reReviewMode into routing.json, the single routing surface read by every consumer review.
review.md The shared reviewer prompt is the reviewer's behavior in every consuming repo; adds prior-review staging, depth dispatch, the flip rule, and the stamp step.
counters.ts Adds the costByRereviewDepth pricing surface the mode dial is judged on.

Common patterns

3 files: New keptBlockingCount / reReviewMode field threaded through paired type declarations and their runtime literals.

2 files: Re-exporting the new routing-config symbols (DEFAULT_RE_REVIEW_MODE, RE_REVIEW_MODES, ReReviewMode) through router.ts.

2 files: keptBlockingCount: 0 added to early-return object literals in rereview.ts and its test.

* squashes, and base merges (which rewrite SHAs and shift line numbers but
* not the change's content), while a deletion-only payload still moves the
* fingerprint. (Step 1's older added-lines-only prompt definition is
* superseded by this CLI; the one-time mismatch on upgrade degrades to a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): The docstring calls Step 1's added-lines-only hunk signature "superseded by this CLI," but the two coexist with different rules: this CLI hashes added and removed lines, while review.md Step 1's comment-scoping signature is still added-lines-only and live, so they disagree on a deletion-only change. Consider unifying on one shared helper, or rewording to say the two signatures are intentionally distinct (comment-scoping vs. divergence).

Low-confidence (1)
  • workflows/review/lib/rereview-mode.ts:328findLatestStamp/parseRereviewStamp: the stamp is unauthenticated and parsed last-match-wins from a body that can carry author-influenced text, so a foreign github-actions[bot] review echoing a stamp-shaped comment and newer than any genuinely-stamped review could anchor the run. Latent for fast/flip-gated only — default full never reads the stamp.

/** Truncation keeps the stamp compact; 16 hex chars ≈ 64 bits per hunk. */
const HUNK_HASH_CHARS = 16;

const HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note (non-blocking): HUNK_HEADER_RE and the hunk-body split are hand-rolled here, but diff.ts (already imported for splitUnifiedDiff) declares an identical HUNK_HEADER_RE (unexported) alongside countHunkHeaders/stripDiffFiles. Exporting a shared hunk-body splitter from diff.ts would keep the grammar in one place so a future diff-format tweak can't drift between the fingerprint and the rest of the pipeline.

@jwbron jwbron changed the base branch from jwbron/review-dispatch-tax to main July 13, 2026 19:06
jwbron added 2 commits July 13, 2026 12:08
…' into jwbron/rereview-mode-dial

# Conflicts:
#	.claude/skills/review-trial/SKILL.md
#	workflows/review/lib/disciplines.test.ts
#	workflows/review/lib/rereview.test.ts
#	workflows/review/lib/rereview.ts
#	workflows/review/review.md
…under the max-lines limit (the main merge pushed router.ts to 1004)
@jwbron jwbron force-pushed the jwbron/rereview-mode-dial branch from 481fb40 to 9b047d5 Compare July 13, 2026 19:15
@jwbron jwbron merged commit f57f77b into main Jul 13, 2026
6 checks passed
@jwbron jwbron deleted the jwbron/rereview-mode-dial branch July 13, 2026 19:16
@jwbron

jwbron commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up for the staging migration discussed above (repo issues are disabled, so recording it here; the round-three plan carries it as step 5's second named slice):

Stage a deterministic caller map

The gap, observed live

The v1.5.0 behavior test (Khan/webapp#40793, seeded per the #40690 methodology) planted a cross-file amplification: a public function's Limit(pageSize) became conditional (pageSize <= 0 disables paging, with a legitimate in-diff consumer on the prune path), while a pre-existing caller in an untouched file computes a page size that can reach the sentinel. The reviewer missed it entirely: no finding on the changed guard, nothing for the claim-validator to judge, nothing dropped at any gate (run 29284668567's artifacts confirm a pure finder miss). The near-miss makes the mechanism precise: first-principles named the exact hazard ("any future caller passing pageSize 0 silently gets an unbounded fetch") as a non-blocking design suggestion, but no agent ever enumerated the EXISTING callers, so "future hazard" was never upgraded to "present amplification" and the introduce-vs-amplify rule never engaged.

Why this is structural

The pipeline deterministically computes facts about the diff (changed-line provenance, snap tables, routing, budgets) but nothing about the diff's dependents. "Who relies on the contract this change narrowed?" is left to model judgment under an investigation budget deliberately designed to discourage speculative exploration, and the camouflage is systematic: a contract change that ships with an in-diff consumer answers the cheap "why does this exist" question without any caller ever being read. Exhortation ("investigate harder") fights the budget discipline; the fix is a missing deterministic input, the same class of gap provenance.json and the anchor-snap table already closed for their questions.

Proposed fix (a named slice of the deterministic-orchestrator staging migration)

  1. callers.json staging step (deterministic CLI in the lib). For each exported symbol whose declaration or guard lines the diff touches, enumerate call sites across the repo (first cut: grep by symbol name filtered by package import; language servers later) and stage symbol -> [file:line + snippet], capped per symbol, with a truncated marker when the cap bites. No model output needed, so it belongs in the pre-workflow staging layer (see the review: the re-review mode dial (full | scoped | flip-gated | fast) #246 review discussion).
  2. One rule in the removed-behavior audit. A public function's guard, limit, or default becoming conditional is removed behavior for every existing caller: read the staged call sites before accepting an in-diff consumer as the justification, and classify introduce-vs-amplify per caller.
  3. Corpus case to price it. A sanitized port of the #40793 D2 shape with a realistically reachable sentinel (small constant, not the original's edge-only arithmetic). It sits in the discriminating 20-80% band and gives the introduce-vs-amplify prose rule (review: state introduce-vs-amplify in the posted prose #250) its first eval coverage. Recall-affecting change, so eval-gated with a targeted-repeats powered run per the memo's recipe.

Deliberately out of scope: an escalation edge that lets a design-altitude observation re-dispatch the correctness reviewer (model-driven control flow exactly where the roadmap removes it), and dynamic-dispatch/cross-service call resolution (real but much smaller residual than today's gap).

Refs: round-three plan step 5 (second named slice) and step 9 (first new mint); the behavior-test scorecard on webapp#40793; eval memo rev 5.1.

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.

2 participants