-
Notifications
You must be signed in to change notification settings - Fork 1
review: the re-review mode dial (full | scoped | flip-gated | fast) #246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jwbron
wants to merge
9
commits into
jwbron/review-dispatch-tax
Choose a base branch
from
jwbron/rereview-mode-dial
base: jwbron/review-dispatch-tax
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
de239ee
[jwbron/rereview-mode-dial] review: the re-review mode dial: ROUTING …
jwbron 1329297
[jwbron/rereview-mode-dial] review: expose keptBlockingCount from the…
jwbron 45c6f6e
[jwbron/rereview-mode-dial] review: wire the re-review mode dial into…
jwbron 976b925
[jwbron/rereview-mode-dial] review: prettier-format the mode-dial fil…
jwbron bbb624c
[jwbron/rereview-mode-dial] Merge branch 'jwbron/review-dispatch-tax'…
jwbron 64f8115
[jwbron/rereview-mode-dial] Merge branch 'jwbron/review-dispatch-tax'…
jwbron 73b55f4
Merge branch 'jwbron/review-dispatch-tax' into jwbron/rereview-mode-dial
jwbron 7eb5501
review: live-enabled corpus format and ten live cases (#233)
jwbron 7b53141
[landtmp] Merge remote-tracking branch 'origin/main' into landtmp
jwbron File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "review": minor | ||
| --- | ||
|
|
||
| The re-review mode dial, the runs-per-PR cost lever: a per-repo `re-review` line in `.github/aw/review/ROUTING` (`full` | `scoped` | `flip-gated` | `fast`, default `full`) decides how much of the roster a repeat review of the same PR runs. `scoped` runs the whole roster staged only the hunks new since the last fully-reviewed fingerprint; `flip-gated` runs reconciliation plus the correctness pass, whose validated blocking findings veto a REQUEST_CHANGES→APPROVE flip; `fast` is reconcile-only. Three deterministic guards (`lib/rereview-mode.ts`): the anchoring full review is taken at ready-for-review (never on a draft skeleton); the flip gate; and a divergence tripwire that stamps a content-hashed hunk signature into the review body as a hidden comment (surviving cache eviction, dismiss-stale-approvals, and COMMENTED-only histories) and re-arms full review when the unreviewed share reaches 0.4. Adversarial lifecycle cases (rewrite-after-approval, sparse-PR-then-payload) live in `eval/lifecycle/` and are replayed deterministically; run cost now aggregates per executed depth (`costByRereviewDepth`) so the live A/B can price `scoped` against `full` on recall and dollars. Ships default-`full`: no consumer's behavior changes until its repo adds a mode line. Motivated by measured lifecycle costs on the seeded trial (v1.3.1: $7.19/$8.77/$9.14; v1.4.0: $8.31/$9.87/$11.29 per push), where 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. Supersedes the consumer-side stopgap in Khan/frontend#13420. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| /** | ||
| * Lifecycle replay for the re-review mode dial: the adversarial cases the | ||
| * dial must survive, as data (`eval/lifecycle/*.json`), replayed | ||
| * deterministically through the same lib functions the production CLI runs. | ||
| * | ||
| * These cases do not fit the corpus format (a corpus case is one PR state; | ||
| * these are push *sequences*), so they live in their own dataset. Each case | ||
| * lists the modes it must hold under, a sequence of pushes (each a unified | ||
| * diff and the verdict that run submits), and per-push expectations. The | ||
| * harness threads the review-body stamp from push to push exactly as | ||
| * production does: compute the signature, decide the depth, then stamp the | ||
| * simulated review the next push reads. | ||
| * | ||
| * The two adversarial cases are scored as "the tripwire re-armed and the | ||
| * payload got a full review"; the economy control is scored as "the reduced | ||
| * path actually ran"; a dial that trips on every fix push is a no-op cost | ||
| * lever, so both directions are load-bearing. A live-PR trial | ||
| * (`.claude/skills/review-trial/SKILL.md`) exercises the same sequences | ||
| * against the real workflow when a lifecycle change ships. | ||
| */ | ||
|
|
||
| import {readdirSync, readFileSync} from "node:fs"; | ||
| import {join} from "node:path"; | ||
|
|
||
| import {describe, it, expect} from "vitest"; | ||
|
|
||
| import { | ||
| computeHunkSignature, | ||
| decideReReviewDepth, | ||
| findLatestStamp, | ||
| renderRereviewStamp, | ||
| STAMP_SCHEMA_VERSION, | ||
| } from "../lib/rereview-mode"; | ||
| import type {PriorReview, ReReviewPlan} from "../lib/rereview-mode"; | ||
| import type {ReReviewMode} from "../lib/routing-config"; | ||
|
|
||
| /* -------------------------------------------------------------------------- */ | ||
| /* Dataset shape and loading */ | ||
| /* -------------------------------------------------------------------------- */ | ||
|
|
||
| type LifecyclePush = { | ||
| name: string; | ||
| isDraft: boolean; | ||
| /** The verdict this run submits (rides into the stamp the next push reads). */ | ||
| verdict: string; | ||
| /** The push's full unified diff, one line per array entry. */ | ||
| diff: string[]; | ||
| expected: { | ||
| /** A depth, or `same-as-mode`: the configured mode's own cheap path. */ | ||
| depth: string; | ||
| tripwireRearmed: boolean; | ||
| }; | ||
| }; | ||
|
|
||
| type LifecycleCase = { | ||
| id: string; | ||
| tags: string[]; | ||
| description: string; | ||
| /** The reduced modes the expectations must hold under, each replayed. */ | ||
| modes: ReReviewMode[]; | ||
| scoring: "payload-full-review" | "reduced-path-taken"; | ||
| pushes: LifecyclePush[]; | ||
| }; | ||
|
|
||
| const LIFECYCLE_DIR = join(__dirname, "lifecycle"); | ||
|
|
||
| const loadLifecycleCases = (): LifecycleCase[] => | ||
| readdirSync(LIFECYCLE_DIR) | ||
| .filter((name) => name.endsWith(".json")) | ||
| .sort() | ||
| .map( | ||
| (name) => | ||
| JSON.parse( | ||
| readFileSync(join(LIFECYCLE_DIR, name), "utf8"), | ||
| ) as LifecycleCase, | ||
| ); | ||
|
|
||
| /* -------------------------------------------------------------------------- */ | ||
| /* Replay */ | ||
| /* -------------------------------------------------------------------------- */ | ||
|
|
||
| /** Replay one case under one mode; returns the per-push plans in order. */ | ||
| const replay = ( | ||
| lifecycleCase: LifecycleCase, | ||
| mode: ReReviewMode, | ||
| ): ReReviewPlan[] => { | ||
| const priorReviews: PriorReview[] = []; | ||
| const plans: ReReviewPlan[] = []; | ||
| for (const [index, push] of lifecycleCase.pushes.entries()) { | ||
| const plan = decideReReviewDepth({ | ||
| mode, | ||
| isDraft: push.isDraft, | ||
| priorStamp: findLatestStamp(priorReviews), | ||
| currentSignature: computeHunkSignature(push.diff.join("\n")), | ||
| }); | ||
| plans.push(plan); | ||
| priorReviews.push({ | ||
| body: renderRereviewStamp({ | ||
| schemaVersion: STAMP_SCHEMA_VERSION, | ||
| depth: plan.depth, | ||
| verdict: push.verdict, | ||
| anchorDraft: plan.stampAnchorDraft, | ||
| anchorHunks: plan.stampHunks, | ||
| }), | ||
| submittedAt: `2026-07-0${index + 1}T00:00:00Z`, | ||
| }); | ||
| } | ||
| return plans; | ||
| }; | ||
|
|
||
| /* -------------------------------------------------------------------------- */ | ||
| /* The suite */ | ||
| /* -------------------------------------------------------------------------- */ | ||
|
|
||
| const CASES = loadLifecycleCases(); | ||
|
|
||
| describe("re-review lifecycle dataset", () => { | ||
| it("loads the adversarial pair and at least one economy control", () => { | ||
| const ids = CASES.map((c) => c.id); | ||
| expect(ids).toContain("rereview-rewrite-after-approval"); | ||
| expect(ids).toContain("rereview-sparse-pr-then-payload"); | ||
| expect(CASES.some((c) => c.scoring === "reduced-path-taken")).toBe( | ||
| true, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe.each(CASES)("$id", (lifecycleCase) => { | ||
| describe.each(lifecycleCase.modes)("under mode %s", (mode) => { | ||
| const plans = replay(lifecycleCase, mode); | ||
|
|
||
| it.each( | ||
| lifecycleCase.pushes.map((push, index) => ({ | ||
| push, | ||
| plan: plans[index], | ||
| })), | ||
| )("push $push.name meets its expected depth", ({push, plan}) => { | ||
| const expectedDepth = | ||
| push.expected.depth === "same-as-mode" | ||
| ? mode | ||
| : push.expected.depth; | ||
| expect(plan.depth).toBe(expectedDepth); | ||
| expect(plan.tripwireRearmed).toBe(push.expected.tripwireRearmed); | ||
| }); | ||
|
|
||
| if (lifecycleCase.scoring === "payload-full-review") { | ||
| it("scores: the tripwire re-armed and the payload got a full review", () => { | ||
| const payload = plans[plans.length - 1]; | ||
| expect(payload.tripwireRearmed).toBe(true); | ||
| expect(payload.depth).toBe("full"); | ||
| expect(payload.dispatch).toBe("all"); | ||
| expect(payload.staging).toBe("whole-diff"); | ||
| }); | ||
| } else { | ||
| it("scores: the reduced path actually ran on the follow-up push", () => { | ||
| const followUp = plans[plans.length - 1]; | ||
| expect(followUp.depth).toBe(mode); | ||
| expect(followUp.tripwireRearmed).toBe(false); | ||
| }); | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| { | ||
| "id": "rereview-fix-push-stays-cheap", | ||
| "tags": ["rereview", "economy"], | ||
| "description": "The economics control: a reviewed six-hunk PR gets a routine one-hunk fix push. The unreviewed share (1/6) stays under the tripwire threshold, so each reduced mode actually runs its cheap path; this is the case the dial exists for, and the case the live A/B prices against full. A dial that trips on every fix push would be a no-op cost lever.", | ||
| "modes": ["scoped", "flip-gated", "fast"], | ||
| "scoring": "reduced-path-taken", | ||
| "pushes": [ | ||
| { | ||
| "name": "initial", | ||
| "isDraft": false, | ||
| "verdict": "REQUEST_CHANGES", | ||
| "diff": [ | ||
| "diff --git a/src/models/order.ts b/src/models/order.ts", | ||
| "--- a/src/models/order.ts", | ||
| "+++ b/src/models/order.ts", | ||
| "@@ -5,2 +5,3 @@", | ||
| " export type Order = {", | ||
| "+ status: OrderStatus;", | ||
| " id: string;", | ||
| "@@ -20,2 +21,3 @@", | ||
| " export const load = (id: string) => {", | ||
| "+ metrics.count(\"order.load\");", | ||
| " return db.get(id);", | ||
| "@@ -40,2 +42,3 @@", | ||
| " export const save = (order: Order) => {", | ||
| "+ metrics.count(\"order.save\");", | ||
| " return db.put(order);", | ||
| "diff --git a/src/models/order.test.ts b/src/models/order.test.ts", | ||
| "--- a/src/models/order.test.ts", | ||
| "+++ b/src/models/order.test.ts", | ||
| "@@ -8,2 +8,3 @@", | ||
| " describe(\"order\", () => {", | ||
| "+ it(\"carries a status\", () => run(statusCase));", | ||
| " it(\"loads\", () => run(loadCase));", | ||
| "@@ -30,2 +31,3 @@", | ||
| " const loadCase = {", | ||
| "+ status: \"open\",", | ||
| " id: \"o-1\",", | ||
| "@@ -50,2 +52,3 @@", | ||
| " const saveCase = {", | ||
| "+ status: \"open\",", | ||
| " id: \"o-2\"," | ||
| ], | ||
| "expected": {"depth": "full", "tripwireRearmed": false} | ||
| }, | ||
| { | ||
| "name": "review-fix", | ||
| "isDraft": false, | ||
| "verdict": "APPROVE", | ||
| "diff": [ | ||
| "diff --git a/src/models/order.ts b/src/models/order.ts", | ||
| "--- a/src/models/order.ts", | ||
| "+++ b/src/models/order.ts", | ||
| "@@ -5,2 +5,3 @@", | ||
| " export type Order = {", | ||
| "+ status: OrderStatus;", | ||
| " id: string;", | ||
| "@@ -20,2 +21,3 @@", | ||
| " export const load = (id: string) => {", | ||
| "+ metrics.count(\"order.load\");", | ||
| " return db.get(id);", | ||
| "@@ -40,2 +42,3 @@", | ||
| " export const save = (order: Order) => {", | ||
| "+ metrics.count(\"order.save\", {status: order.status});", | ||
| " return db.put(order);", | ||
| "diff --git a/src/models/order.test.ts b/src/models/order.test.ts", | ||
| "--- a/src/models/order.test.ts", | ||
| "+++ b/src/models/order.test.ts", | ||
| "@@ -8,2 +8,3 @@", | ||
| " describe(\"order\", () => {", | ||
| "+ it(\"carries a status\", () => run(statusCase));", | ||
| " it(\"loads\", () => run(loadCase));", | ||
| "@@ -30,2 +31,3 @@", | ||
| " const loadCase = {", | ||
| "+ status: \"open\",", | ||
| " id: \"o-1\",", | ||
| "@@ -50,2 +52,3 @@", | ||
| " const saveCase = {", | ||
| "+ status: \"open\",", | ||
| " id: \"o-2\"," | ||
| ], | ||
| "expected": {"depth": "same-as-mode", "tripwireRearmed": false} | ||
| } | ||
| ] | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.tsabsorbs per-tool read errors and throws only on run-levelsubtype !== "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.