Skip to content

feat: flag what changed since the reviewer's last look - #89

Merged
lbildzinkas merged 3 commits into
masterfrom
fm/second-look-since-last-look
Oct 7, 2026
Merged

lbildzinkas merged 3 commits into
masterfrom
fm/second-look-since-last-look

Conversation

@lbildzinkas

@lbildzinkas lbildzinkas commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Intent

Implements "Since your last look" (closes #46). The companion records the head commit each time the reviewer opens a review. On a later open, the parts changed since that last look are flagged, and a filter shows only those parts. After a force-push or rebase, the change itself is compared rather than the raw commits, so upstream changes are not mixed in. If the old commit can no longer be compared, the companion says so and treats every part as changed. Without a local record, the last look falls back to the commit of the reviewer's last submitted GitHub review.

Acceptance criteria: tests cover a plain push, a rebase, a force-push with an edit, and a missing old commit; the filter shows only changed parts, and the panel says which commit the last look was at.

What Changed

  • The engine records the head commit of each review the reviewer opens in the pull request's cache folder and compares the change with the one at the last look — each against its own merge base, so a rebase or force-push compares the changes themselves and mixes in nothing from upstream — falling back to the commit of the reviewer's last submitted GitHub review without a local record, and counting every part as changed when the old commit is gone or no longer related; the outcome rides the review result as sinceLastLook (schema version 16).
  • The review tree flags each part changed since the last look, the line above the tree says which commit that look was at (or why the change could not be compared), and a new filter button/command shows only the changed parts, resetting per pull request; the overview repeats the last-look line under the meta.
  • Adds engine tests for the first look, a plain push, a rebase onto newer master, a force-push with an edit, and a missing old commit, plus tree, overview and integration tests for the new surfaces, and documents the feature in the README.

Risk Assessment

✅ Low: The feature is well-bounded, mirrors the existing reviewed-marks store patterns, is covered by behavioral tests against real git repositories and a fake GitHub, keeps all new GitHub access read-only under the reviewer's token with only local commit-id/timestamp storage, and every substantiated finding is an info-level edge case or docs nit.

Testing

Drove the engine's last-look scenarios live earlier in this run (recorded evidence covers plain push, rebase with/without an edit, force-push that rebased and edited, old commit gone, 422 compare, and the GitHub-review fallback) and confirmed them again through the engine unit tests; the extension side was validated per the operator's decision through its unit and stub-based integration tests plus rendered output, showing the flagged part, the filter showing only it, the commit-naming line above the tree and in the overview panel, and the corrected could-not-be-compared wording; everything passed and the worktree is clean. Per the live-validation contract, the scenarios covered only by unit, stub, or rendered-output evidence (no live product drive in this run) are reported as untested, not pass.

  • Live validation: ⚠️ inconclusive - 8 of 14 scenarios driven live against the product
Scenario Result Live Evidence
First look at a pull request records the head commit and flags nothing ✅ pass live ~/.no-mistakes/evidence/01M4AQ2XR00WWP1NZ0YK180005/live-engine-first-look-records-and-plain-push-flags-only-the-edit.json (step 1) plus `npx vitest run packages/engine/test/last-lo…
Plain push with one edit: only the edited part is flagged ✅ pass live live-engine-first-look-records-and-plain-push-flags-only-the-edit.json (steps 2-3) plus last-look.test.ts 'flags only the part a plain push changed'
Rebase onto a newer master without a real edit flags nothing (upstream changes do not mix in) ✅ pass live live-engine-rebase-onto-newer-master-flags-nothing.json plus last-look.test.ts 'flags nothing after a rebase onto a newer master, whatever upstream changed'
Ticket's hand-check: rebase onto newer master with one real edit flags only the edited part ✅ pass live live-engine-rebase-then-one-real-edit-flags-only-the-edit.json plus last-look.test.ts 'flags only the part a plain push changed' rebase cases
Force-push that rebased and edited flags only the edited part ✅ pass live live-engine-force-push-that-rebased-and-edited-flags-only-the-edit.json plus last-look.test.ts 'flags only the edited part after a force-push that rebased and edited'
Old commit gone from GitHub: outcome is 'not compared' and every part counts as changed ✅ pass live live-engine-old-commit-gone-counts-everything-as-changed.json (recorded against the fix commit: outcome 'not compared') plus last-look.test.ts 'counts every part as changed when GitHub no longer has t…
Compare answers 422 (commits share no history): everything counts as changed and the commit is not claimed gone (adversarial) ✅ pass live live-engine-compare-answers-422-counts-everything-as-changed.json plus last-look.test.ts 'counts every part as changed when the compare answers 422, as for commits sharing no history'
No local record: last look falls back to the commit of the reviewer's last submitted GitHub review ✅ pass live live-engine-no-local-record-falls-back-to-last-github-review.json plus last-look.test.ts "falls back to the commit of the reviewer's last submitted GitHub review"
Re-opening at the same commit still compares with the look before (adversarial: no self-compare) ⏸️ untested no The prior payload supported this only with the unit test last-look.test.ts 'keeps comparing with the look before when the review opens again at the same commit', which runs against a disposable git re…
Extension flags the changed part and the filter button shows only it, with the message naming the last-look commit ⏸️ untested no The prior payload drove this only through the stub-based integration suite ('since your last look' tests against a vscode stub and a spawned fake engine) and a rendered artifact bundled from the real…
Overview panel says which commit the last look was at and what changed ⏸️ untested no The prior payload supported this only with overview.test.ts unit assertions and a rendered overviewHtml artifact; the real webview panel was never rendered inside a running VS Code in this run (operat…
First look has nothing to filter and says so (adversarial: filter on first look) ⏸️ untested no The prior payload covered this only via the stub-based integration test 'has nothing to filter on a first look' and tree.test.ts unit cases, both against the vscode stub rather than the live product;…
Filter resets for a different pull request's first result and keeps across re-reviews of the same one (review fix) ⏸️ untested no The prior payload covered this review fix only through the stub-based integration test 'keeps the only-changed filter across re-reviews of one pull request and clears it for another', which drives Rev…
Could-not-compare wording no longer claims the commit is gone (review fix, both tree tooltip and panel line) ⏸️ untested no The prior payload established the new wording only through tree.test.ts and overview.test.ts assertions plus rendered artifacts bundled from the real code; the wording was never observed in the real r…
Evidence: Live engine protocol: first look records, plain push flags only the edit
{
  "scenario": "first-look-records-and-plain-push-flags-only-the-edit",
  "steps": [
    {
      "what": "review opened",
      "head": "3d5db00",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "flagged": []
    },
    {
      "what": "author pushes one commit editing util.txt line 12",
      "pushed": "bcd57f9"
    },
    {
      "what": "review opened",
      "head": "bcd57f9",
      "parts": [
        "app.txt",
        "util.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "ranking the parts with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "sinceLastLook": {
        "commit": "3d5db00",
        "from": "local record",
        "at": "2026-10-07T09:13:45.032Z",
        "outcome": "compared",
        "changedPieces": 1
      },
      "flagged": [
        "util.txt"
      ]
    },
    {
      "what": "last-look.json persisted by the first engine process",
      "content": {
        "version": 1,
        "last": {
          "commit": "bcd57f907929696cb981ee1909971ef7a59b4735",
          "at": "2026-10-07T09:13:45.337Z"
        },
        "before": {
          "commit": "3d5db009b52827e0e22f64af04beb1fa69e160f2",
          "at": "2026-10-07T09:13:45.032Z"
        }
      }
    }
  ],
  "verdict": {
    "pass": true
  },
  "ok": true
}
Evidence: Live engine protocol: rebase then one real edit flags only the edit
{
  "scenario": "rebase-then-one-real-edit-flags-only-the-edit",
  "steps": [
    {
      "what": "review opened",
      "head": "d7589d7",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "flagged": []
    },
    {
      "what": "rebased onto newer master, then one real edit of app.txt line 30",
      "looked": "d7589d7",
      "rebasedTo": "293eb96"
    },
    {
      "what": "review opened",
      "head": "293eb96",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "sinceLastLook": {
        "commit": "d7589d7",
        "from": "local record",
        "at": "2026-10-07T09:13:46.249Z",
        "outcome": "compared",
        "changedPieces": 1
      },
      "flagged": [
        "app.txt"
      ]
    }
  ],
  "verdict": {
    "pass": true
  },
  "ok": true
}
Evidence: Live engine protocol: rebase onto newer master flags nothing
{
  "scenario": "rebase-onto-newer-master-flags-nothing",
  "steps": [
    {
      "what": "review opened",
      "head": "0941947",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "flagged": []
    },
    {
      "what": "upstream edits a context line of the first hunk, the other file, adds one file; feature rebased",
      "looked": "0941947",
      "rebasedTo": "01756f9"
    },
    {
      "what": "review opened",
      "head": "01756f9",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "sinceLastLook": {
        "commit": "0941947",
        "from": "local record",
        "at": "2026-10-07T09:13:45.659Z",
        "outcome": "compared",
        "changedPieces": 0
      },
      "flagged": []
    }
  ],
  "verdict": {
    "pass": true
  },
  "ok": true
}
Evidence: Live engine protocol: force-push that rebased and edited flags only the edit
{
  "scenario": "force-push-that-rebased-and-edited-flags-only-the-edit",
  "steps": [
    {
      "what": "review opened",
      "head": "f3fba81",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "flagged": []
    },
    {
      "what": "rebased onto newer master and amended with an edit of app.txt line 30 (force-push)",
      "looked": "f3fba81",
      "forcedTo": "06231fb"
    },
    {
      "what": "review opened",
      "head": "06231fb",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "sinceLastLook": {
        "commit": "f3fba81",
        "from": "local record",
        "at": "2026-10-07T09:13:46.876Z",
        "outcome": "compared",
        "changedPieces": 1
      },
      "flagged": [
        "app.txt"
      ]
    }
  ],
  "verdict": {
    "pass": true
  },
  "ok": true
}
Evidence: Live engine protocol: old commit gone counts everything as changed
{
  "scenario": "old-commit-gone-counts-everything-as-changed",
  "steps": [
    {
      "what": "review opened",
      "head": "42f64c6",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "flagged": []
    },
    {
      "what": "force-push with an edit, then the old commit collected away",
      "looked": "42f64c62d3b2f4b7fa25b1eab8c6d44b7d4f6430",
      "lookedStillOnGitHub": false,
      "forcedTo": "12a9781"
    },
    {
      "what": "review opened",
      "head": "12a9781",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "sinceLastLook": {
        "commit": "42f64c6",
        "from": "local record",
        "at": "2026-10-07T09:13:47.487Z",
        "outcome": "not compared",
        "changedPieces": 0
      },
      "flagged": [
        "app.txt"
      ]
    }
  ],
  "verdict": {
    "pass": true
  },
  "ok": true
}
Evidence: Live engine protocol: compare answers 422 counts everything as changed
{
  "scenario": "compare-answers-422-counts-everything-as-changed",
  "steps": [
    {
      "what": "review opened",
      "head": "42f64c6",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "flagged": []
    },
    {
      "what": "force-push with an edit; GitHub answers 422 for the compare while the old commit still exists",
      "looked": "42f64c62d3b2f4b7fa25b1eab8c6d44b7d4f6430",
      "lookedStillOnGitHub": true
    },
    {
      "what": "review opened",
      "head": "860222c",
      "parts": [
        "app.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "sinceLastLook": {
        "commit": "42f64c6",
        "from": "local record",
        "at": "2026-10-07T09:13:48.053Z",
        "outcome": "not compared",
        "changedPieces": 0
      },
      "flagged": [
        "app.txt"
      ]
    }
  ],
  "verdict": {
    "pass": true
  },
  "ok": true
}
Evidence: Live engine protocol: no local record falls back to last GitHub review
{
  "scenario": "no-local-record-falls-back-to-last-github-review",
  "steps": [
    {
      "what": "pull request history",
      "reviewedAt": "f5c12b8",
      "head": "cfc1420"
    },
    {
      "what": "review opened",
      "head": "cfc1420",
      "parts": [
        "app.txt",
        "util.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "ranking the parts with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "ranking the parts with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "flagged": []
    },
    {
      "what": "no local record; the reviewer's last submitted GitHub review was at the earlier commit"
    },
    {
      "what": "review opened",
      "head": "cfc1420",
      "parts": [
        "app.txt",
        "util.txt"
      ],
      "stages": [
        "grouping related hunks with pi",
        "ranking the parts with pi",
        "writing the story with pi",
        "listing the claims with pi",
        "grouping related hunks with pi",
        "ranking the parts with pi",
        "writing the story with pi",
        "listing the claims with pi"
      ],
      "sinceLastLook": {
        "commit": "f5c12b8",
        "from": "github review",
        "at": "2026-10-02T10:00:00Z",
        "outcome": "compared",
        "changedPieces": 1
      },
      "flagged": [
        "util.txt"
      ]
    }
  ],
  "verdict": {
    "pass": true
  },
  "ok": true
}
Evidence: Rendered extension tree: flagged part, filter showing only it, message lines naming the last-look commit, not-compared wording

message: Since your last look at abcdef0 on 2026-10-01: 1 of 7 parts changed. ▸ Must review src/retry.py — changed since your last look · New code the send path now runs on every delivery. … filtered: ▸ Must review src/retry.py — changed since your last look · New code the send path now runs on every delivery. message: … Showing only those.

TREE — every part (message above the tree):
message: Since your last look at abcdef0 on 2026-10-01: 1 of 7 parts changed.

▸ Must review
    src/retry.py — changed since your last look · New code the send path now runs on every delivery.
    tooltip: new code
▸ Worth reviewing
    src/settings.ts — Changed code that the retry policy reads.
    tooltip: changed code
▸ Context
    CHANGELOG.md — Release note only.
    tooltip: documentation only
▸ Not ranked yet
    src/legacy.ts
    __tests__/retry.test.ts.snap
    tooltip: snapshot · claimed — Only known snapshot names are matched.
▸ Noise
    uv.lock — lockfile · claimed
    tooltip: Only known lockfile names are matched.
    transport.py — moved or renamed · confirmed
    tooltip: Identical content proves only the move.

TREE — after the reviewer toggles "Show only changed since last look":
message: Since your last look at abcdef0 on 2026-10-01: 1 of 7 parts changed. Showing only those.

▸ Must review
    src/retry.py — changed since your last look · New code the send path now runs on every delivery.
    tooltip: new code

TREE — the change could not be compared with the last look:
message: The change could not be compared with your last look at abcdef0 on 2026-10-01, because that commit is gone or no longer related: every part counts as changed.

▸ Must review
    src/retry.py — changed since your last look · New code the send path now runs on every delivery.
    tooltip: new code
▸ Worth reviewing
    src/settings.ts — changed since your last look · Changed code that the retry policy reads.
    tooltip: changed code
▸ Context
    CHANGELOG.md — changed since your last look · Release note only.
    tooltip: documentation only
▸ Not ranked yet
    src/legacy.ts — changed since your last look
    tooltip: Counts as changed: the change could not be compared with your last look at abcdef0, because that commit is gone or no longer related.
    __tests__/retry.test.ts.snap — changed since your last look
    tooltip: snapshot · claimed — Only known snapshot names are matched.
▸ Noise
    uv.lock — changed since your last look · lockfile · claimed
    tooltip: Only known lockfile names are matched.
    transport.py — changed since your last look · moved or renamed · confirmed
    tooltip: Identical content proves only the move.
Evidence: Rendered overview panel HTML: since line under the meta, compared and not-compared variants

Since your last look at abcdef0 on 2026-10-01: 1 of 7 parts changed. / The change could not be compared with your last look at abcdef0 on 2026-10-01, because that commit is gone or no longer related: every part counts as changed.

<!doctype html><html><head><meta charset="utf-8"><title>Second Look overview panel — Since your last look</title>
<style>body{font-family:-apple-system,Segoe UI,sans-serif;margin:2rem;background:#1e1e1e;color:#ccc}iframe{width:100%;height:640px;border:1px solid #444;background:#fff}h1{font-size:1.1rem}pre{white-space:pre-wrap;color:#9cdcfe;background:#252526;padding:1rem;border:1px solid #444}</style>
</head><body><h1>Second Look overview panel — Since your last look</h1><iframe srcdoc="<!DOCTYPE html>
<html lang=&quot;en&quot;>
<head>
<meta charset=&quot;UTF-8&quot;>
<meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;default-src 'none'; style-src 'nonce-NONCE'; script-src 'nonce-NONCE';&quot;>
<style nonce=&quot;NONCE&quot;>
  body {
    color: var(--vscode-foreground);
    background-color: var(--vscode-editor-background);
    font-family: var(--vscode-font-family);
    font-size: var(--vscode-font-size, 13px);
    margin: 0;
    padding: 0 26px;
  }
  main { max-width: 900px; padding: 18px 0 48px; }
  h1 { font-size: 20px; font-weight: 600; margin: 0 0 4px; }
  h2 { font-size: 15px; font-weight: 600; margin: 16px 0 8px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .meta, .note { color: var(--vscode-descriptionForeground); font-size: 12px; }
  .note { margin: 0 0 6px; }
  .stages { display: flex; gap: 6px; flex-wrap: wrap; margin: 10px 0 4px; }
  .stg { font-size: 12px; border: 1px solid var(--vscode-panel-border); border-radius: 12px; padding: 1px 9px; }
  .stg.done::before { content: &quot;✓ &quot;; color: var(--vscode-testing-iconPassed, #89d185); }
  .stg.run { color: var(--vscode-descriptionForeground); }
  .stamp { font-size: 11px; font-weight: 400; color: var(--vscode-descriptionForeground); }
  .story {
    border: 1px solid var(--vscode-panel-border);
    border-left: 3px solid var(--vscode-textLink-foreground);
    border-radius: 3px;
    padding: 10px 12px;
    font-size: 13.5px;
    line-height: 1.55;
  }
  .sentence.focus { background-color: var(--vscode-editor-findMatchHighlightBackground); }
  .pt {
    font: inherit;
    color: var(--vscode-textLink-foreground);
    background: none;
    border: none;
    border-bottom: 1px dotted var(--vscode-textLink-foreground);
    padding: 0;
    cursor: pointer;
  }
  .pt.focus { font-weight: 600; }
  .issue { font-style: italic; }
  code, .shown { font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
  .description {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border: 1px solid var(--vscode-panel-border);
    border-radius: 3px;
    padding: 10px 12px;
  }
  .alert {
    border-left: 3px solid var(--vscode-editorWarning-foreground);
    padding: 4px 10px;
    margin: 0 0 8px;
  }
  .hidden {
    border: 1px dashed var(--vscode-editorWarning-foreground);
    border-radius: 3px;
    padding: 0 4px;
  }
  .flag {
    color: var(--vscode-editorWarning-foreground);
    font-size: 11px;
    font-weight: 600;
    margin-right: 6px;
  }
  .claims { padding-left: 22px; margin: 0; }
  .claims li { margin-bottom: 8px; }
  .quote { overflow-wrap: anywhere; }
  .where { color: var(--vscode-descriptionForeground); font-size: 12px; margin-top: 2px; }
  .verdict { font-style: italic; }
  .verdict.finding { color: var(--vscode-editorWarning-foreground); font-weight: 600; }
  .why { color: var(--vscode-descriptionForeground); font-size: 12px; overflow-wrap: anywhere; }
  .evidence { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px 10px; font-size: 12px; margin-top: 2px; }
  .evidence .label, .cited { color: var(--vscode-descriptionForeground); }
  .evidence span { min-width: 0; overflow-wrap: anywhere; }
  .cite { font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
  .none { color: var(--vscode-editorWarning-foreground); }
  .findings, .checks { padding-left: 18px; margin: 0; }
  .findings li, .checks li { margin-bottom: 6px; overflow-wrap: anywhere; }
  .att, .sev, .check { font-size: 11px; font-weight: 600; border: 1px solid var(--vscode-panel-border); border-radius: 10px; padding: 0 7px; }
  .att.stale, .att.malformed, .sev.error, .sev.warning, .check.failed { color: var(--vscode-editorWarning-foreground); }
  .att.fresh, .check.passed { color: var(--vscode-testing-iconPassed, #89d185); }
  .log {
    font-family: var(--vscode-editor-font-family, monospace);
    font-size: 12px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border: 1px solid var(--vscode-panel-border);
    border-radius: 3px;
    padding: 6px 10px;
    margin: 4px 0;
    max-height: 320px;
    overflow-y: auto;
  }
  .stamps { padding-left: 18px; margin: 0; }
  .stamps li { margin-bottom: 4px; }
</style>
</head>
<body>
<main>
  <h1>Retry failed webhook sends</h1>
  <div class=&quot;meta&quot;>example-org/example-repo #42 · reviewer-login · retry-webhooks → master · head f00dcaf</div>
  <div class=&quot;meta since&quot;>Since your last look at abcdef0 on 2026-10-01: 1 of 7 parts changed.</div>
  <div class=&quot;stages&quot;><span class=&quot;stg done&quot;>parts</span><span class=&quot;stg done&quot;>noise checks</span></div>
  <section id=&quot;story&quot;><h2>Story</h2><p class=&quot;note&quot;>No story was written for this review.</p></section>
  <section id=&quot;criteria&quot;><h2>Acceptance criteria</h2><p class=&quot;note&quot;>No criteria were read for this review.</p></section>
  <section id=&quot;unexplained&quot;><h2>Unexplained changes</h2><p class=&quot;note&quot;>The change was not compared with its description and issues for this review.</p></section>
  <section id=&quot;claims&quot;><h2>Claims</h2><p class=&quot;note&quot;>No claims were listed for this review.</p></section>
  <section id=&quot;pipeline&quot;><h2>Pipeline and CI</h2><p><span class=&quot;att missing&quot;>no-mistakes report: none</span> <span class=&quot;note&quot;>the description carries no no-mistakes attestation.</span></p><p class=&quot;note&quot;>No CI was read for this review.</p></section>
  <section id=&quot;description&quot;><h2>Pull request description</h2><div class=&quot;description&quot;>A description of any length, kept in full.</div></section>
  <section id=&quot;stamps&quot;><h2>How these results were made</h2><ul class=&quot;stamps&quot;><li><b>Parts</b> grouped by the plain pass</li><li><b>Ranking</b> ranked by the plain rule</li></ul></section>
</main>
<script nonce=&quot;NONCE&quot;>
(function () {
  'use strict';
  var vscode = acquireVsCodeApi();
  Array.prototype.forEach.call(document.querySelectorAll('button.pt[data-part]'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'openPart', part: Number(button.getAttribute('data-part')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.issue'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'openIssue', issue: Number(button.getAttribute('data-issue')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.cite'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({
        type: 'openEvidence',
        criterion: Number(button.getAttribute('data-criterion')),
        evidence: button.getAttribute('data-evidence'),
        index: Number(button.getAttribute('data-index'))
      });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.draft'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'draft', finding: button.getAttribute('data-draft'), index: Number(button.getAttribute('data-index')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.manual'), function (button) {
    button.addEventListener('click', function () {
      var description = document.getElementById('description');
      if (description !== null) {
        description.scrollIntoView({ block: 'start' });
      }
    });
  });
  var focused = document.querySelector('.sentence.focus');
  if (focused !== null) {
    focused.scrollIntoView({ block: 'center' });
  }
}());
</script>
</body>
</html>"></iframe>
     <h1>Overview when the change could not be compared</h1>
     <iframe srcdoc="<!DOCTYPE html>
<html lang=&quot;en&quot;>
<head>
<meta charset=&quot;UTF-8&quot;>
<meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;default-src 'none'; style-src 'nonce-NONCE'; script-src 'nonce-NONCE';&quot;>
<style nonce=&quot;NONCE&quot;>
  body {
    color: var(--vscode-foreground);
    background-color: var(--vscode-editor-background);
    font-family: var(--vscode-font-family);
    font-size: var(--vscode-font-size, 13px);
    margin: 0;
    padding: 0 26px;
  }
  main { max-width: 900px; padding: 18px 0 48px; }
  h1 { font-size: 20px; font-weight: 600; margin: 0 0 4px; }
  h2 { font-size: 15px; font-weight: 600; margin: 16px 0 8px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .meta, .note { color: var(--vscode-descriptionForeground); font-size: 12px; }
  .note { margin: 0 0 6px; }
  .stages { display: flex; gap: 6px; flex-wrap: wrap; margin: 10px 0 4px; }
  .stg { font-size: 12px; border: 1px solid var(--vscode-panel-border); border-radius: 12px; padding: 1px 9px; }
  .stg.done::before { content: &quot;✓ &quot;; color: var(--vscode-testing-iconPassed, #89d185); }
  .stg.run { color: var(--vscode-descriptionForeground); }
  .stamp { font-size: 11px; font-weight: 400; color: var(--vscode-descriptionForeground); }
  .story {
    border: 1px solid var(--vscode-panel-border);
    border-left: 3px solid var(--vscode-textLink-foreground);
    border-radius: 3px;
    padding: 10px 12px;
    font-size: 13.5px;
    line-height: 1.55;
  }
  .sentence.focus { background-color: var(--vscode-editor-findMatchHighlightBackground); }
  .pt {
    font: inherit;
    color: var(--vscode-textLink-foreground);
    background: none;
    border: none;
    border-bottom: 1px dotted var(--vscode-textLink-foreground);
    padding: 0;
    cursor: pointer;
  }
  .pt.focus { font-weight: 600; }
  .issue { font-style: italic; }
  code, .shown { font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
  .description {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border: 1px solid var(--vscode-panel-border);
    border-radius: 3px;
    padding: 10px 12px;
  }
  .alert {
    border-left: 3px solid var(--vscode-editorWarning-foreground);
    padding: 4px 10px;
    margin: 0 0 8px;
  }
  .hidden {
    border: 1px dashed var(--vscode-editorWarning-foreground);
    border-radius: 3px;
    padding: 0 4px;
  }
  .flag {
    color: var(--vscode-editorWarning-foreground);
    font-size: 11px;
    font-weight: 600;
    margin-right: 6px;
  }
  .claims { padding-left: 22px; margin: 0; }
  .claims li { margin-bottom: 8px; }
  .quote { overflow-wrap: anywhere; }
  .where { color: var(--vscode-descriptionForeground); font-size: 12px; margin-top: 2px; }
  .verdict { font-style: italic; }
  .verdict.finding { color: var(--vscode-editorWarning-foreground); font-weight: 600; }
  .why { color: var(--vscode-descriptionForeground); font-size: 12px; overflow-wrap: anywhere; }
  .evidence { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px 10px; font-size: 12px; margin-top: 2px; }
  .evidence .label, .cited { color: var(--vscode-descriptionForeground); }
  .evidence span { min-width: 0; overflow-wrap: anywhere; }
  .cite { font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
  .none { color: var(--vscode-editorWarning-foreground); }
  .findings, .checks { padding-left: 18px; margin: 0; }
  .findings li, .checks li { margin-bottom: 6px; overflow-wrap: anywhere; }
  .att, .sev, .check { font-size: 11px; font-weight: 600; border: 1px solid var(--vscode-panel-border); border-radius: 10px; padding: 0 7px; }
  .att.stale, .att.malformed, .sev.error, .sev.warning, .check.failed { color: var(--vscode-editorWarning-foreground); }
  .att.fresh, .check.passed { color: var(--vscode-testing-iconPassed, #89d185); }
  .log {
    font-family: var(--vscode-editor-font-family, monospace);
    font-size: 12px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border: 1px solid var(--vscode-panel-border);
    border-radius: 3px;
    padding: 6px 10px;
    margin: 4px 0;
    max-height: 320px;
    overflow-y: auto;
  }
  .stamps { padding-left: 18px; margin: 0; }
  .stamps li { margin-bottom: 4px; }
</style>
</head>
<body>
<main>
  <h1>Retry failed webhook sends</h1>
  <div class=&quot;meta&quot;>example-org/example-repo #42 · reviewer-login · retry-webhooks → master · head f00dcaf</div>
  <div class=&quot;meta since&quot;>The change could not be compared with your last look at abcdef0 on 2026-10-01, because that commit is gone or no longer related: every part counts as changed.</div>
  <div class=&quot;stages&quot;><span class=&quot;stg done&quot;>parts</span><span class=&quot;stg done&quot;>noise checks</span></div>
  <section id=&quot;story&quot;><h2>Story</h2><p class=&quot;note&quot;>No story was written for this review.</p></section>
  <section id=&quot;criteria&quot;><h2>Acceptance criteria</h2><p class=&quot;note&quot;>No criteria were read for this review.</p></section>
  <section id=&quot;unexplained&quot;><h2>Unexplained changes</h2><p class=&quot;note&quot;>The change was not compared with its description and issues for this review.</p></section>
  <section id=&quot;claims&quot;><h2>Claims</h2><p class=&quot;note&quot;>No claims were listed for this review.</p></section>
  <section id=&quot;pipeline&quot;><h2>Pipeline and CI</h2><p><span class=&quot;att missing&quot;>no-mistakes report: none</span> <span class=&quot;note&quot;>the description carries no no-mistakes attestation.</span></p><p class=&quot;note&quot;>No CI was read for this review.</p></section>
  <section id=&quot;description&quot;><h2>Pull request description</h2><div class=&quot;description&quot;>A description of any length, kept in full.</div></section>
  <section id=&quot;stamps&quot;><h2>How these results were made</h2><ul class=&quot;stamps&quot;><li><b>Parts</b> grouped by the plain pass</li><li><b>Ranking</b> ranked by the plain rule</li></ul></section>
</main>
<script nonce=&quot;NONCE&quot;>
(function () {
  'use strict';
  var vscode = acquireVsCodeApi();
  Array.prototype.forEach.call(document.querySelectorAll('button.pt[data-part]'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'openPart', part: Number(button.getAttribute('data-part')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.issue'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'openIssue', issue: Number(button.getAttribute('data-issue')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.cite'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({
        type: 'openEvidence',
        criterion: Number(button.getAttribute('data-criterion')),
        evidence: button.getAttribute('data-evidence'),
        index: Number(button.getAttribute('data-index'))
      });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.draft'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'draft', finding: button.getAttribute('data-draft'), index: Number(button.getAttribute('data-index')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.manual'), function (button) {
    button.addEventListener('click', function () {
      var description = document.getElementById('description');
      if (description !== null) {
        description.scrollIntoView({ block: 'start' });
      }
    });
  });
  var focused = document.querySelector('.sentence.focus');
  if (focused !== null) {
    focused.scrollIntoView({ block: 'center' });
  }
}());
</script>
</body>
</html>"></iframe></body></html>
- Outcome: ⚠️ 1 warning across 2 runs (35m59s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ packages/extension/src/overview.ts:298 - The doc comment "A chip for each stage done, and one for the stage still running." now sits stacked above sinceLine()'s own doc instead of on stageChips() (overview.ts:305), so it documents the wrong function and stageChips is left undocumented. Mechanical move of the comment to stageChips; no behavior change.
  • ℹ️ packages/extension/src/extension.ts:260 - ReviewSession.onlyChanged is never reset when a review of a different pull request shows its first result (show()'s !update branch, extension.ts:391-396). Concrete sequence: the reviewer toggles the filter on PR A, then reviews PR B; on PR B's second look sinceLastLook is defined, so the tree silently starts filtered (buildTree at extension.ts:414 and the "Showing only those." message at extension.ts:426) without the reviewer ever pressing the button for PR B. The message line does disclose it, so impact is mild; the remedy — clearing onlyChanged when a new review's first result arrives — is a user-visible behavior decision, so it needs the author's call rather than an automated change.
  • ℹ️ packages/engine/src/last-look.ts:101 - changePieces hashes with apart=false, so two identical hunks of one file share a single hash. Concrete sequence: a file already holds a hunk adding lines A at the last look; the author then adds a second, identical hunk elsewhere in the same file. compareWith (last-look.ts:153-154) dedupes both to one piece that the old change already held, so sinceLastLook.changed is empty, the panel reads "0 parts changed" and the filter hides the genuinely new addition. This is the documented tradeoff for regrouping stability (a part holding only one of the identical hunks must hash it the same as the whole-file computation), and the same content was already reviewed once; per-file occurrence numbering cannot be reconstructed from a part subset. Noted as an accepted limitation, not a defect to harden.
  • ℹ️ packages/engine/src/github.ts:243 - getChangeDiff maps any 422 from compareCommitsWithBasehead to the 'commit gone' outcome, but 422 also occurs when base and head have no common ancestor (for example after the pull request's base branch was switched to an unrelated branch) — a state where the old head commit still exists on GitHub. The panel and tooltips (packages/extension/src/tree.ts:302-305, 337-339) then assert "GitHub no longer has <commit>", which is factually wrong, though the everything-counts-as-changed fallback is the safe direction. Rare edge; distinguishing 422 causes from the error body would be the only remedy.

🔧 Fix applied.
1 info still open:

  • ℹ️ packages/engine/src/last-look.ts:101 - changePieces hashes with apart=false, so two identical hunks of one file share a single hash. Concrete sequence: a file already holds a hunk adding lines A at the last look; the author then adds a second, identical hunk elsewhere in the same file. compareWith (last-look.ts:153-154) dedupes both to one piece that the old change already held, so sinceLastLook.changed is empty, the panel reads "0 parts changed" and the filter hides the genuinely new addition. This is the documented tradeoff for regrouping stability (a part holding only one of the identical hunks must hash it the same as the whole-file computation), and the same content was already reviewed once; per-file occurrence numbering cannot be reconstructed from a part subset. Noted as an accepted limitation, not a defect to harden.
⚠️ **Test** - 1 warning
  • ⚠️ The Test agent did not finish within its invocation budget. Reported: agent run tests timed out after 30m0s: agent last produced output 6s ago (249 observed); agent reported: pi exited: exit status 143. This is a budget or provider-slowness cut, not a code failure. Re-running the same request costs another full budget, so no further attempt is made automatically. If this repository's targeted tests or evidence gathering routinely approach the default 30m0s, raise test_agent_timeout in global config. Respond with fix to spend another budget: a repair turn runs only for selected findings other than this budget cut, then validation re-runs. Or abort and retry after raising the budget.
  • 🚨 Approval is refused: the run worktree at ~/.no-mistakes/worktrees/a09930ebf9a5/01M4AQ2XR00WWP1NZ0YK180005 holds work no Test turn validated, and the steps after Test would commit and publish it. It holds uncommitted changes to .live-tmp/ (inspect with git -C ~/.no-mistakes/worktrees/a09930ebf9a5/01M4AQ2XR00WWP1NZ0YK180005 status and git -C ~/.no-mistakes/worktrees/a09930ebf9a5/01M4AQ2XR00WWP1NZ0YK180005 diff). Respond with fix to validate it, or abort.

🔧 No changes applied.
1 warning still open:

  • ⚠️ live validation verdict: inconclusive (8 of 14 scenarios were driven live against the product); untested: Re-opening at the same commit still compares with the look before (adversarial: no self-compare), Extension flags the changed part and the filter button shows only it, with the message naming the last-look commit, Overview panel says which commit the last look was at and what changed, First look has nothing to filter and says so (adversarial: filter on first look), Filter resets for a different pull request's first result and keeps across re-reviews of the same one (review fix), Could-not-compare wording no longer claims the commit is gone (review fix, both tree tooltip and panel line)
  • Live validation: ⚠️ inconclusive - 8 of 14 scenarios driven live against the product
Scenario Result Live Evidence
First look at a pull request records the head commit and flags nothing ✅ pass live ~/.no-mistakes/evidence/01M4AQ2XR00WWP1NZ0YK180005/live-engine-first-look-records-and-plain-push-flags-only-the-edit.json (step 1) plus `npx vitest run packages/engine/test/last-lo…
Plain push with one edit: only the edited part is flagged ✅ pass live live-engine-first-look-records-and-plain-push-flags-only-the-edit.json (steps 2-3) plus last-look.test.ts 'flags only the part a plain push changed'
Rebase onto a newer master without a real edit flags nothing (upstream changes do not mix in) ✅ pass live live-engine-rebase-onto-newer-master-flags-nothing.json plus last-look.test.ts 'flags nothing after a rebase onto a newer master, whatever upstream changed'
Ticket's hand-check: rebase onto newer master with one real edit flags only the edited part ✅ pass live live-engine-rebase-then-one-real-edit-flags-only-the-edit.json plus last-look.test.ts 'flags only the part a plain push changed' rebase cases
Force-push that rebased and edited flags only the edited part ✅ pass live live-engine-force-push-that-rebased-and-edited-flags-only-the-edit.json plus last-look.test.ts 'flags only the edited part after a force-push that rebased and edited'
Old commit gone from GitHub: outcome is 'not compared' and every part counts as changed ✅ pass live live-engine-old-commit-gone-counts-everything-as-changed.json (recorded against the fix commit: outcome 'not compared') plus last-look.test.ts 'counts every part as changed when GitHub no longer has t…
Compare answers 422 (commits share no history): everything counts as changed and the commit is not claimed gone (adversarial) ✅ pass live live-engine-compare-answers-422-counts-everything-as-changed.json plus last-look.test.ts 'counts every part as changed when the compare answers 422, as for commits sharing no history'
No local record: last look falls back to the commit of the reviewer's last submitted GitHub review ✅ pass live live-engine-no-local-record-falls-back-to-last-github-review.json plus last-look.test.ts "falls back to the commit of the reviewer's last submitted GitHub review"
Re-opening at the same commit still compares with the look before (adversarial: no self-compare) ⏸️ untested no The prior payload supported this only with the unit test last-look.test.ts 'keeps comparing with the look before when the review opens again at the same commit', which runs against a disposable git re…
Extension flags the changed part and the filter button shows only it, with the message naming the last-look commit ⏸️ untested no The prior payload drove this only through the stub-based integration suite ('since your last look' tests against a vscode stub and a spawned fake engine) and a rendered artifact bundled from the real…
Overview panel says which commit the last look was at and what changed ⏸️ untested no The prior payload supported this only with overview.test.ts unit assertions and a rendered overviewHtml artifact; the real webview panel was never rendered inside a running VS Code in this run (operat…
First look has nothing to filter and says so (adversarial: filter on first look) ⏸️ untested no The prior payload covered this only via the stub-based integration test 'has nothing to filter on a first look' and tree.test.ts unit cases, both against the vscode stub rather than the live product;…
Filter resets for a different pull request's first result and keeps across re-reviews of the same one (review fix) ⏸️ untested no The prior payload covered this review fix only through the stub-based integration test 'keeps the only-changed filter across re-reviews of one pull request and clears it for another', which drives Rev…
Could-not-compare wording no longer claims the commit is gone (review fix, both tree tooltip and panel line) ⏸️ untested no The prior payload established the new wording only through tree.test.ts and overview.test.ts assertions plus rendered artifacts bundled from the real code; the wording was never observed in the real r…
  • npx vitest run packages/engine/test/last-look.test.ts — 10 passed (first look records, plain push, rebase, force-push+edit, old commit gone, 422 no-common-history, GitHub-review fallback, same-commit re-open, corrupt record, hunk hashing)
  • npx vitest run packages/engine/test/server.test.ts packages/engine/test/review.test.ts packages/engine/test/cli.test.ts — 67 passed (protocol carries sinceLastLook; looks recorded beside reviewed marks)
  • npx vitest run packages/extension/test/tree.test.ts packages/extension/test/overview.test.ts packages/extension/test/review-result.test.ts packages/extension/test/engine-client.test.ts — 151 passed (flag descriptions/tooltips, filter, since line, overview meta line, protocol guard, wiring)
  • npx vitest run --config vitest.integration.config.ts -t &#39;since your last look&#39; — 3 passed against the vscode stub and a spawned fake engine: flag+filter+message line, first-look message, filter kept across re-reviews of one PR and cleared for another PR
  • Rendered-output check: bundled the real overviewHtml, buildTree and treeMessage with the real mixedResult fixture via repo-local esbuild and wrote rendered artifacts (overview HTML with the since line in compared and not-compared variants; tree text unfiltered, filtered, and not-compared) into the evidence directory
  • Cleanup per operator instruction: rm -rf .live-tmp .vscode-test — the worktree is clean with no untracked files
⚠️ **Document** - 1 info
  • ℹ️ docs/ux/README.md:22 - The frozen design record's 'Second visit' screen says 'The diff shows only what changed since the last look, and the companion says which findings were resolved and who replied'; the shipped feature filters the review tree (not the diff) and does not report resolved findings or replies. docs/ux is a record of the design comparison, like an ADR, and README step 7 now owns the current behavior, so I left it untouched; whether to annotate the divergence in the design record is the author's call.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Each review the reviewer opens records its head commit in the pull request's cache folder. On a later open, the engine compares the change at that commit with the change now, each against its own merge base and by each hunk's changed lines alone, so a rebase or a force-push mixes in nothing from upstream; the result carries the pieces that changed. When GitHub no longer has that commit, the result says so and every part counts as changed. With no local record, the last look is the commit of the reviewer's last submitted GitHub review.

The tree flags each part changed since the last look, a filter button shows only those parts, and the line above the tree and the overview say which commit the last look was at and how many parts changed.

Closes #46
@lbildzinkas
lbildzinkas merged commit a979f73 into master Oct 7, 2026
5 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.

Since your last look

1 participant