Skip to content

feat: mark parts reviewed, kept locally by content hash - #88

Merged
lbildzinkas merged 6 commits into
masterfrom
fm/second-look-reviewed-marks
Oct 7, 2026
Merged

lbildzinkas merged 6 commits into
masterfrom
fm/second-look-reviewed-marks

Conversation

@lbildzinkas

@lbildzinkas lbildzinkas commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Intent

Closes #45. Each part in the review tree gets a reviewed checkbox. Marks are stored locally in the pull request's own cache folder, keyed by the part's content hash, so a part whose content changes is unmarked automatically and says "changed since you marked it", while untouched parts keep their marks. The tree view's badge counts the parts left to review. An opt-in setting, off by default because the GitHub Pull Requests extension syncs the same field, marks a file "Viewed" on GitHub once every part in it is reviewed.

The acceptance criteria are that marks persist across restarts in the local per-pull-request store, that content-hash invalidation has tests, and that the GitHub mirror is off by default and only marks whole files. Validation runs through the engine protocol and unit tests without launching VS Code or writing to GitHub; tests exercise the mirror only against fake GitHub responses.

What Changed

  • Each part in the review tree gets a reviewed checkbox, persisted by the engine in a per-pull-request local store (reviewed-marks.json) keyed by the part's content hash (per-hunk pieces without line numbers, blob ids for binary files), so marks survive restarts and a part whose content changed is unmarked and flagged "changed since you marked it"; the tree view's badge counts the parts left. New reviewedMarks and markReviewed JSON-RPC methods read and update the store.
  • A new opt-in second-look.mirrorViewedToGitHub setting (off by default, since the GitHub Pull Requests extension syncs the same field) mirrors fully-reviewed files to GitHub's "Viewed" through a new markViewed RPC and a GraphQL markFileAsViewed mutation; files only partly reviewed are never marked, and nothing is ever unmarked.
  • Grouping validation now rejects duplicate and reserved agent part names (marks key on part identity), and the diff parser keeps binary files' blob ids so their content hash tracks their content; CONTEXT.md gains the Reviewed mark glossary entry and README documents the checkbox, badge and mirror setting.

Risk Assessment

✅ Low: Both user-prescribed fixes verify in source, the identity invariant now holds across every reachable part source I could construct, tests are behavioral and match the acceptance criteria, and no new defect survived adversarial tracing of the pipeline-authored commits.

Testing

Exercised the reviewed-marks feature at every layer available without launching VS Code or touching real GitHub, as the intent prescribes: the full unit suite passes; the real engine binary and real engine server processes were driven live over their stdio JSON-RPC protocol, proving restart-persistent local marks, content-hash invalidation that unmarks only a pushed part, coexisting marks for same-named parts (declaration merging), and a mirror that marks only whole files with the request's token and never fires on its own; the mirror's off-by-default was checked only non-live (declarative contribution boolean default false plus zero-mutation logs, with the extension-side gate owned by the CI integration test), and the badge and agent-fallback scenarios were likewise not driven live. Lint was not run (phase rule); the extension UI surface (checkbox rendering, badge display) is covered by unit tests and the CI-owned integration suite because the intent forbids launching VS Code in validation.

  • Live validation: ✅ go - 4 of 7 scenarios driven live against the product
Scenario Result Live Evidence
A reviewer ticks a part's checkbox; the mark lands in the pull request's local store and survives an engine restart (and clearing removes it, malformed parts are refused) ✅ pass live live-a-engine-binary-marks-transcript.txt — the real engine binary (dist/main.js serve --cache-dir …) driven over stdio JSON-RPC across two process lifetimes, with the persisted reviewed-marks.json sh…
Mark parts, push a change to one of them, review again: only that part is unmarked and reads "changed since marked"; re-ticking restores it ✅ pass live live-b-engine-server-review-mirror-push-transcript.txt (B2) — full re-review through a new engine process against the changed diff, same local store: exactly the interface part reads 'changed since ma…
Two parts of one file that share a display name (TypeScript declaration merging of interface Config and class Config) can both be ticked and both read reviewed ✅ pass live live-b-engine-server-review-mirror-push-transcript.txt (B2) and live-b-pull-99-store-after-push.json — two parts both named 'Config in src/config.ts', two coexisting marks with kind-discriminated iden…
The opt-in GitHub "Viewed" mirror marks only whole files, using the request's own token, and refuses a part-reviewed file, a path no part holds, and a pull request with no finished review — calling Gi… ✅ pass live live-b-engine-server-review-mirror-push-transcript.txt (B1/B2) plus the fake-GitHub request logs — exercised against the real engine server with the intent's sanctioned injected fake fetch; one mutati…
The mirror is off by default: with default settings nothing is ever written to GitHub ⏸️ untested no The prior payload did not establish a live result (it recorded live=false): the engine-side zero-mutation logs, the declarative boolean default in packages/extension/package.json, and the CI-owned int…
The view badge counts what is left to review and disappears once every part is reviewed ⏸️ untested no The prior payload did not establish a live result (it recorded live=false): only the underlying partsLeft count was observed live in the engine protocol, while the badge itself was covered solely by u…
An agent grouping answer that names two parts alike is rejected, retried once, and falls back to the plain grouping (round-1 fix) ⏸️ untested no The prior payload did not establish a live result (it recorded live=false): the only coverage was the unit test packages/engine/test/grouping.test.ts, which is not a live drive. Driving this live requ…
Evidence: Live drive A transcript: real engine binary, marks persist across restart

engine process 1 (real binary, isolated cache dir) ok: initialize answers the version handshake ok: a pull request nobody marked holds no marks ok: ticking a part answers with one mark keyed by its content hash ok: the mark carries the part identity and its pieces ok: reading the marks back answers the same engine process 2 (a restart, same cache dir) ok: marks wait for the version handshake ok: the mark survived the restart in the local store ok: clearing the checkbox empties the store ok: a part whose pieces are not sha256 hashes is refused ok: a request whose url is not a pull request is refused ok: the part ticks again after being refused and cleared ok: the store on disk holds the mark under its content hash LIVE A PASSED

engine process 1 (real binary, isolated cache dir)
  ok: initialize answers the version handshake
  ok: a pull request nobody marked holds no marks
  ok: ticking a part answers with one mark keyed by its content hash
  ok: the mark carries the part identity and its pieces
  ok: reading the marks back answers the same
engine process 2 (a restart, same cache dir)
  ok: marks wait for the version handshake
  ok: the mark survived the restart in the local store
  ok: clearing the checkbox empties the store
  ok: a part whose pieces are not sha256 hashes is refused
  ok: a request whose url is not a pull request is refused
  ok: the part ticks again after being refused and cleared
  ok: the store on disk holds the mark under its content hash

store file .tmp-live/cache-a/github.com/example-org/example-repo/pull-42/reviewed-marks.json:
{
  "version": 1,
  "marks": {
    "5e9ae866add9a85d69c3481d059bb9f158a39e5670ba11f95112fc409630894e": {
      "name": "[\"Cart.total in web/cart.ts\",[\"web/cart.ts\"],[\"class Cart\"]]",
      "pieces": [
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
      ],
      "markedAt": "2026-10-06T16:18:06.770Z"
    }
  }
}

LIVE A PASSED
Evidence: Live drive B transcript: review, whole-file mirror, same-named parts, pushed change
B1: review the recorded pull 7, mirror its files
  ok: the review answers with its result
  parts of the review:
    - fresh in app/fresh.py (app/fresh.py)
    - test_fresh in tests/test_fresh.py (tests/test_fresh.py)
    - apply_discount in app/dedent.py (app/dedent.py)
    - scripts/deploy.rb (scripts/deploy.rb)
    - top-level code in web/cart.ts (web/cart.ts)
    - load, Store, Store.__init__ and 1 more in app/reformat.py (app/reformat.py)
    - Greeter, Greeter.Greet in src/Greeter.cs (src/Greeter.cs)
  ok: a whole review mirrors nothing on its own
  ok: every part of the review holds a mark
  ok: every part reads reviewed
  ok: nothing is left to review (the badge count)
  ok: marking touched GitHub for nothing
  ok: every wholly reviewed file is marked "Viewed"
  ok: one mutation per file reached GitHub, each with the request’s token
  ok: a path no part holds is not marked
  ok: no mutation named the stranger path
  ok: an engine with no finished review refuses the mirror
  ok: the refusal called GitHub for nothing
B2: two parts sharing one display name, and a pushed change (pull 99)
  parts of the review:
    - Config in src/config.ts (src/config.ts) adds:   timeout: number;
    - Config in src/config.ts (src/config.ts) adds:   retries: number;
  ok: the TypeScript declaration merge makes two parts of one file
  ok: the two parts share one display name
  ok: the first same-named part holds a mark
  ok: a file with a part left unreviewed is not marked "Viewed"
  ok: nothing reached GitHub for the partly reviewed file
  ok: both same-named parts hold a mark at once
  ok: their marks differ in identity
  ok: both read reviewed
  ok: nothing is left to review (the badge count)
  ok: the whole file counts as reviewed
  ok: the wholly reviewed file is marked "Viewed"
  ok: the pushed change still reviews as two parts
  ok: the marks survived into the new review
  ok: exactly the interface’s part changed
  ok: only the changed part reads changed since marked
  ok: exactly one part is left to review
  ok: the file no longer mirrors while one part changed
  ok: GitHub was asked for nothing after the push
  ok: re-ticking the changed part restores the file
  ok: the file mirrors again once every part is reviewed
LIVE B PASSED
Evidence: Persisted local store after the push: two same-named Config parts hold distinct kind-discriminated marks

{ "version": 1, "marks": { "3363…": { "name": "["Config in src/config.ts",["src/config.ts"],["class Config"]]", … }, "2274…": { "name": "["Config in src/config.ts",["src/config.ts"],["interface Config"]]", … } } }

{
  "version": 1,
  "marks": {
    "336300e26ebcb79282f3bb6f5bc330caa9367ab3c4df86c4b48a6d5a7dc1092f": {
      "name": "[\"Config in src/config.ts\",[\"src/config.ts\"],[\"class Config\"]]",
      "pieces": [
        "88b414020b4d49ce2189e6196384837a845477fdf814725a2c24b4ceeceba115"
      ],
      "markedAt": "2026-10-06T16:20:07.887Z"
    },
    "227425275fa9a5d5c418808ceefc16fa04bb9e9ed39d395e5c3d31c0efd0645a": {
      "name": "[\"Config in src/config.ts\",[\"src/config.ts\"],[\"interface Config\"]]",
      "pieces": [
        "4f3cee1a737ea055d73fd7eafc53014cf971230dfb92bf9f644d7d1d8dad23e8"
      ],
      "markedAt": "2026-10-06T16:20:08.016Z"
    }
  }
}
Evidence: Fake-GitHub request log, pull 7 drive: markFileAsViewed mutations only from explicit markViewed, each with the request token
{"url":"https://api.github.com/repos/example-org/example-repo/pulls/7","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/pulls/7","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/contents/.gitattributes?ref=7777777777777777777777777777777777777777","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/compare/5555555555555555555555555555555555555555...7777777777777777777777777777777777777777?per_page=1","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/commits/7777777777777777777777777777777777777777/check-runs?per_page=100","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"query($owner: String!, $name: String!, $number: Int!) {\n  repository(owner: $owner, name: $name) {\n    defaultBranchRef { name }\n    pullRequest(number: $number) {\n      closingIssuesReferences(first: 50) {\n        nodes { number title url body repository { nameWithOwner } }\n      }\n      timelineItems(first: 100, itemTypes: CROSS_REFERENCED_EVENT) {\n        nodes { ... on CrossReferencedEvent { source { ... on Issue { number title url body repository { nameWithOwner } } } } }\n      }\n    }\n  }\n}","variables":{"owner":"example-org","name":"example-repo","number":7}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/tarball/7777777777777777777777777777777777777777","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/tarball/6666666666666666666666666666666666666666","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"query($owner: String!, $name: String!, $number: Int!) {\n  repository(owner: $owner, name: $name) { pullRequest(number: $number) { id } }\n}","variables":{"owner":"example-org","name":"example-repo","number":7}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"app/fresh.py"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"tests/test_fresh.py"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"app/dedent.py"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"scripts/deploy.rb"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"web/cart.ts"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"app/reformat.py"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"src/Greeter.cs"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"query($owner: String!, $name: String!, $number: Int!) {\n  repository(owner: $owner, name: $name) { pullRequest(number: $number) { id } }\n}","variables":{"owner":"example-org","name":"example-repo","number":7}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"app/fresh.py"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"tests/test_fresh.py"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"app/dedent.py"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"scripts/deploy.rb"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"web/cart.ts"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"app/reformat.py"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"src/Greeter.cs"}},"authorization":"token ghp_live-drive-token"}
Evidence: Fake-GitHub request log, pull 99 drive: zero mutations during review and marking, mirror only after every part is reviewed
{"url":"https://api.github.com/repos/example-org/example-repo/pulls/99","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/pulls/99","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/contents/.gitattributes?ref=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/compare/0000000000000000000000000000000000000000...bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?per_page=1","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/commits/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/check-runs?per_page=100","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"query($owner: String!, $name: String!, $number: Int!) {\n  repository(owner: $owner, name: $name) {\n    defaultBranchRef { name }\n    pullRequest(number: $number) {\n      closingIssuesReferences(first: 50) {\n        nodes { number title url body repository { nameWithOwner } }\n      }\n      timelineItems(first: 100, itemTypes: CROSS_REFERENCED_EVENT) {\n        nodes { ... on CrossReferencedEvent { source { ... on Issue { number title url body repository { nameWithOwner } } } } }\n      }\n    }\n  }\n}","variables":{"owner":"example-org","name":"example-repo","number":99}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/tarball/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/tarball/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"query($owner: String!, $name: String!, $number: Int!) {\n  repository(owner: $owner, name: $name) { pullRequest(number: $number) { id } }\n}","variables":{"owner":"example-org","name":"example-repo","number":99}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"src/config.ts"}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/pulls/99","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/pulls/99","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/contents/.gitattributes?ref=cccccccccccccccccccccccccccccccccccccccc","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/compare/0000000000000000000000000000000000000000...cccccccccccccccccccccccccccccccccccccccc?per_page=1","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/commits/cccccccccccccccccccccccccccccccccccccccc/check-runs?per_page=100","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"query($owner: String!, $name: String!, $number: Int!) {\n  repository(owner: $owner, name: $name) {\n    defaultBranchRef { name }\n    pullRequest(number: $number) {\n      closingIssuesReferences(first: 50) {\n        nodes { number title url body repository { nameWithOwner } }\n      }\n      timelineItems(first: 100, itemTypes: CROSS_REFERENCED_EVENT) {\n        nodes { ... on CrossReferencedEvent { source { ... on Issue { number title url body repository { nameWithOwner } } } } }\n      }\n    }\n  }\n}","variables":{"owner":"example-org","name":"example-repo","number":99}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/repos/example-org/example-repo/tarball/cccccccccccccccccccccccccccccccccccccccc","method":"GET","body":null,"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"query($owner: String!, $name: String!, $number: Int!) {\n  repository(owner: $owner, name: $name) { pullRequest(number: $number) { id } }\n}","variables":{"owner":"example-org","name":"example-repo","number":99}},"authorization":"token ghp_live-drive-token"}
{"url":"https://api.github.com/graphql","method":"POST","body":{"query":"mutation($id: ID!, $path: String!) {\n  markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }\n}","variables":{"id":"PR_kwDOLiveFixture99","path":"src/config.ts"}},"authorization":"token ghp_live-drive-token"}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 packages/engine/src/reviewed-marks.ts:118 - The invariant the round-1 fix established — part names are unique within one review, because applyMark replaces every mark of the same name (reviewed-marks.ts:118) and reviewedState treats a name match as a prior mark (reviewed-marks.ts:75) — is still violated by the plain grouping, which is the default path when no agent is configured. Round 1's analysis claimed the plain grouping guarantees unique names, but it unions hunks by entityKey = ${kind} ${name} (packages/engine/src/parts.ts:9, used at parts.ts:79) while partName renders only entity.name (parts.ts:27-38), dropping the kind. Concrete sequence during intended usage: a TypeScript file declares both interface Config and class Config (declaration merging — kinds 'interface' and 'class' both exist in TYPESCRIPT_ENTITIES, languages.ts:29-43); a pull request edits the interface in one hunk and the class in another; the two hunks share no entity key, so splitFile makes two parts, both named "Config in src/config.ts". The reviewer ticks part A — mark stored; ticks part B — A's mark is deleted by the name filter; A reads 'changed since marked' via the name-match clause; re-ticking A deletes B's mark: the badge never reaches 0 and wholeFilesReviewed (reviewed-marks.ts:91) keeps the file out of the GitHub mirror — the same failure mode as the round-1 error the user chose to fix, left behind by the fix round (2181c17) which closed only the agent-answer path in groupingProblems. Remaining sibling site of the same invariant: packages/engine/src/grouping.ts:296 concatenates the agent's parts with the sinking files' plain parts ([...parts, ...noise]) without checking names across the two sources, so an agent part named exactly like a noise part's plain name (e.g. a bare lockfile path) collides identically. Earliest supported boundary: make the names unique where they are generated — in partName, include the entity's kind (or another discriminator) when parts of one file would otherwise share a name, and reserve the noise parts' names in the agent answer check (or dedupe where the review's parts are assembled) — so the name key holds for every source of parts, not just agent answers.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 7 scenarios driven live against the product
Scenario Result Live Evidence
A reviewer ticks a part's checkbox; the mark lands in the pull request's local store and survives an engine restart (and clearing removes it, malformed parts are refused) ✅ pass live live-a-engine-binary-marks-transcript.txt — the real engine binary (dist/main.js serve --cache-dir …) driven over stdio JSON-RPC across two process lifetimes, with the persisted reviewed-marks.json sh…
Mark parts, push a change to one of them, review again: only that part is unmarked and reads "changed since marked"; re-ticking restores it ✅ pass live live-b-engine-server-review-mirror-push-transcript.txt (B2) — full re-review through a new engine process against the changed diff, same local store: exactly the interface part reads 'changed since ma…
Two parts of one file that share a display name (TypeScript declaration merging of interface Config and class Config) can both be ticked and both read reviewed ✅ pass live live-b-engine-server-review-mirror-push-transcript.txt (B2) and live-b-pull-99-store-after-push.json — two parts both named 'Config in src/config.ts', two coexisting marks with kind-discriminated iden…
The opt-in GitHub "Viewed" mirror marks only whole files, using the request's own token, and refuses a part-reviewed file, a path no part holds, and a pull request with no finished review — calling Gi… ✅ pass live live-b-engine-server-review-mirror-push-transcript.txt (B1/B2) plus the fake-GitHub request logs — exercised against the real engine server with the intent's sanctioned injected fake fetch; one mutati…
The mirror is off by default: with default settings nothing is ever written to GitHub ⏸️ untested no The prior payload did not establish a live result (it recorded live=false): the engine-side zero-mutation logs, the declarative boolean default in packages/extension/package.json, and the CI-owned int…
The view badge counts what is left to review and disappears once every part is reviewed ⏸️ untested no The prior payload did not establish a live result (it recorded live=false): only the underlying partsLeft count was observed live in the engine protocol, while the badge itself was covered solely by u…
An agent grouping answer that names two parts alike is rejected, retried once, and falls back to the plain grouping (round-1 fix) ⏸️ untested no The prior payload did not establish a live result (it recorded live=false): the only coverage was the unit test packages/engine/test/grouping.test.ts, which is not a live drive. Driving this live requ…
  • npm ci (dependencies from package-lock.json into the worktree)
  • npm run build
  • npx tsc -p tsconfig.test.json
  • npx vitest run (1234 unit tests, 69 files; extension integration excluded as CI-owned)
  • npx vitest run packages/engine/test/reviewed-marks.test.ts packages/engine/test/grouping.test.ts packages/extension/test/tree.test.ts packages/extension/test/engine-client.test.ts (91 tests)
  • npx vitest run packages/engine/test/server.test.ts -t "reviewed marks" (5 tests)
  • node .tmp-live/drive-a.mjs — live: real engine binary (packages/engine/dist/main.js serve --cache-dir) over stdio JSON-RPC; initialize/reviewedMarks/markReviewed tick, read-back, restart persistence, clear, malformed-part and bad-URL refusal; persisted reviewed-marks.json inspected
  • node .tmp-live/drive-b.mjs — live: real runRpcServer engine child processes over stdio with fixture-backed fetch (repo fixtures for pull 7, custom declaration-merging pull 99); full review, tick all parts, markViewed whole-files-only with per-request token, stranger-path and no-finished-review refusals with zero mutations, both same-named parts marked at once, pushed change unmarking only the interface part, re-tick restoring the file
  • node --input-type=module semantic parse of packages/extension/package.json: second-look.mirrorViewedToGitHub contributed as boolean with default false
🔧 **Document** - 1 issue found → auto-fixed ✅
  • ℹ️ CONTEXT.md:1 - The change coins "reviewed mark" as core domain vocabulary — used throughout README.md, the second-look.mirrorViewedToGitHub setting description, and the reviewedMarks/markReviewed protocol methods — but the project glossary (CONTEXT.md), the authoritative vocabulary document, has no entry for it, while comparable acting-on-the-review terms (Comment, Draft comment) do. No existing glossary entry was made stale, so per scope discipline I did not add one; whether to add a Reviewed mark entry (e.g. under "Acting on the review", covering the local per-pull-request store, content-hash invalidation, and the opt-in GitHub Viewed mirror) is a vocabulary decision for the owner.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Each part in the tree gets a reviewed checkbox. Marks live in the pull request's cache folder, keyed by the part's content hash and covering the hashes of its pieces (each hunk without its line numbers, or a hunkless file with its blob ids), so a part whose content changes is unmarked and says it changed since it was marked. The view badge counts the parts left. An opt-in setting, off by default, marks a file Viewed on GitHub once every part in it is reviewed.

Closes #45
…n groupingProblems with retry/fallback tests
…os/ubuntu/windows) had one root cause: the CI-only real-host and package-smoke tests still asserted that a clean review round trip sends exactly 2 engine requests (initialize, review), but this PR's reviewed-marks feature legitimately sends a third — the extension calls engine.reviewedMarks(url) immediately after dispatching the review request (extension.ts engineReview → readMarks), and the fake engine logs every request, so the assertion failed with 3 !== 2. Invariant: a test pinning the engine's request log must enumerate every request the extension legitimately sends in the flow it drives. Both sibling sites (packages/extension/test/real-host/run.ts and packages/extension/test/package-smoke/run.ts) got the same smallest correction: expect 3 requests and assert the third is reviewedMarks with params { url }, mirroring the order already proven deterministically by the integration tests (['initialize', 'review', 'reviewedMarks']; stdin writes are synchronous and the review command resolves only after marksRead). No production code changed; later sendReview/draftComment lookups use find over the log and are unaffected, and tree expectations are unchanged (no marks exist in the fake store; renderedItem does not read checkboxState). Verified locally per the intent's constraints (no VS Code launch, no GitHub writes): npm run check passes fully (build, typecheck, lint, 1234 tests including the integration tests for the same request sequence), and tsc -p on both changed test projects compiles. The real VS Code runs remain CI-only as designed
@lbildzinkas
lbildzinkas merged commit 8ada30a 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.

Reviewed marks

1 participant