From 1560ad2659123e9e948bb752b226f940d3c88a86 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 17:17:17 +0000 Subject: [PATCH 1/7] refactor(review): name the note byte check for size, not bounds `noteBounds.ts` measures a note's serialized *bytes*, but CLAUDE.md reserves "bounds" for one concrete visible extent, and the codebase honors that spatially everywhere else (`rowBounds`, `hunkBounds`, `selectedNoteBounds` in DiffPane, `VerticalBounds`). This was the one vocabulary break, and it collided greppably with the spatial note-card concept in the same feature area. Rename the module to `noteSize.ts` and `reviewNoteWithinBounds` to `reviewNoteWithinSizeLimit`; `MAX_REVIEW_NOTE_BYTES` and `reviewNoteByteLength` were already unambiguous and stay put. The conformance fixture file and its exports follow, along with the audit doc's references to the current code. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4 --- docs/browser-review-seam-audit.md | 10 +++++----- .../{noteBounds.test.ts => noteSize.test.ts} | 8 ++++---- .../review/{noteBounds.ts => noteSize.ts} | 4 ++-- src/session/reviewProtocol.test.ts | 6 +++--- src/session/reviewProtocol.ts | 2 +- test/review-conformance/conformance.test.ts | 20 +++++++++---------- .../consumers/reviewWire.ts | 6 +++--- .../{noteBounds.ts => noteSize.ts} | 20 +++++++++---------- test/review-conformance/wireFixtures.ts | 2 +- 9 files changed, 39 insertions(+), 39 deletions(-) rename src/core/review/{noteBounds.test.ts => noteSize.test.ts} (87%) rename src/core/review/{noteBounds.ts => noteSize.ts} (92%) rename test/review-conformance/{noteBounds.ts => noteSize.ts} (90%) diff --git a/docs/browser-review-seam-audit.md b/docs/browser-review-seam-audit.md index 9774138d8..01813b706 100644 --- a/docs/browser-review-seam-audit.md +++ b/docs/browser-review-seam-audit.md @@ -381,17 +381,17 @@ path suffixes, expansion retention, git-status badges). against `MAX_REVIEW_NOTE_BYTES`; broker/producer check whole-note JSON — so a note that passes action validation can poison the entire snapshot with a capacity error. Neither client pre-checks size, and the server's action-body cap is smaller than the largest - "valid" note. Fix: one `reviewNoteWithinBounds` used by wire, broker, producer, and both + "valid" note. Fix: one `reviewNoteWithinSizeLimit` used by wire, broker, producer, and both composers. - _Repaid (Phase 2, core and producer sites)_: `core/review/noteBounds.ts` measures the whole + _Repaid (Phase 2, core and producer sites)_: `core/review/noteSize.ts` measures the whole note in the unit a transport pays — its serialized bytes, through the platform-free `utf8ByteLength` — and `MAX_REVIEW_NOTE_BYTES` sits beside it. Fixtures - `test/review-conformance/noteBounds.ts` pin the boundary the two prototype rules disagreed + `test/review-conformance/noteSize.ts` pin the boundary the two prototype rules disagreed at, including a note whose summary, rationale, and markup each fit while the note itself is three times the bound. Wire and composer sites adopt it in Phases 3 and 5. _Repaid (Phase 3, wire site)_: `isTransportableReviewNote` in `src/session/reviewProtocol.ts` - is `reviewNoteWithinBounds` and nothing else — the wire has no per-field check any more, and - declares no second bound. The protocol module is registered as a consumer of the note-bounds + is `reviewNoteWithinSizeLimit` and nothing else — the wire has no per-field check any more, and + declares no second bound. The protocol module is registered as a consumer of the note-size corpus, so `every-field-fits-but-the-note-does-not` — the note whose summary, rationale, and markup each pass a per-field check while the note is triple the bound — is now refused at the wire rather than admitted and then failing at the publisher. Both composer sites are Phase 5. diff --git a/src/core/review/noteBounds.test.ts b/src/core/review/noteSize.test.ts similarity index 87% rename from src/core/review/noteBounds.test.ts rename to src/core/review/noteSize.test.ts index 1391e85c2..a22902b30 100644 --- a/src/core/review/noteBounds.test.ts +++ b/src/core/review/noteSize.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { MAX_REVIEW_NOTE_BYTES, reviewNoteByteLength, reviewNoteWithinBounds } from "./noteBounds"; +import { MAX_REVIEW_NOTE_BYTES, reviewNoteByteLength, reviewNoteWithinSizeLimit } from "./noteSize"; import type { ReviewNoteV1 } from "./types"; const base: ReviewNoteV1 = { @@ -11,7 +11,7 @@ const base: ReviewNoteV1 = { editable: true, }; -describe("review note bounds", () => { +describe("review note size", () => { test("measures the whole note, framing included", () => { const empty = reviewNoteByteLength(base); expect(empty).toBeGreaterThan(0); @@ -33,12 +33,12 @@ describe("review note bounds", () => { summary: "x".repeat(MAX_REVIEW_NOTE_BYTES - 1), rationale: "x".repeat(MAX_REVIEW_NOTE_BYTES - 1), }; - expect(reviewNoteWithinBounds(oversized)).toBe(false); + expect(reviewNoteWithinSizeLimit(oversized)).toBe(false); }); test("counts multibyte text in bytes rather than characters", () => { const summary = "🧪".repeat(MAX_REVIEW_NOTE_BYTES / 4); expect(summary.length).toBeLessThan(MAX_REVIEW_NOTE_BYTES); - expect(reviewNoteWithinBounds({ ...base, summary })).toBe(false); + expect(reviewNoteWithinSizeLimit({ ...base, summary })).toBe(false); }); }); diff --git a/src/core/review/noteBounds.ts b/src/core/review/noteSize.ts similarity index 92% rename from src/core/review/noteBounds.ts rename to src/core/review/noteSize.ts index 03dad723e..76910ce3d 100644 --- a/src/core/review/noteBounds.ts +++ b/src/core/review/noteSize.ts @@ -30,7 +30,7 @@ export function reviewNoteByteLength(note: ReviewNoteV1) { return utf8ByteLength(JSON.stringify(note)); } -/** Whether one note fits within the shared bound. */ -export function reviewNoteWithinBounds(note: ReviewNoteV1) { +/** Whether one note fits within the shared size limit. */ +export function reviewNoteWithinSizeLimit(note: ReviewNoteV1) { return reviewNoteByteLength(note) <= MAX_REVIEW_NOTE_BYTES; } diff --git a/src/session/reviewProtocol.test.ts b/src/session/reviewProtocol.test.ts index 621a11a09..1148bab77 100644 --- a/src/session/reviewProtocol.test.ts +++ b/src/session/reviewProtocol.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { REVIEW_INTENT_TYPES, type ReviewIntent } from "../core/review/intents"; -import { MAX_REVIEW_NOTE_BYTES, reviewNoteWithinBounds } from "../core/review/noteBounds"; +import { MAX_REVIEW_NOTE_BYTES, reviewNoteWithinSizeLimit } from "../core/review/noteSize"; import { REVIEW_CANONICAL_FILE_CONTENT_TYPE, REVIEW_PATCH_CONTENT_TYPE, @@ -406,12 +406,12 @@ describe("note transport bounds", () => { for (const field of [note.summary, note.rationale!, note.markup!]) { expect(field.length).toBeLessThanOrEqual(MAX_REVIEW_NOTE_BYTES); } - expect(reviewNoteWithinBounds(note)).toBe(false); + expect(reviewNoteWithinSizeLimit(note)).toBe(false); }); test("accepts a note within the shared bound", () => { expect( - reviewNoteWithinBounds({ + reviewNoteWithinSizeLimit({ id: "user:1", source: "user", fileKey: FILE_KEY, diff --git a/src/session/reviewProtocol.ts b/src/session/reviewProtocol.ts index 058d52019..2f41d9ae2 100644 --- a/src/session/reviewProtocol.ts +++ b/src/session/reviewProtocol.ts @@ -11,7 +11,7 @@ * Here the vocabulary *is* `REVIEW_INTENT_TYPES`, and each action's payload is the * intent's own shape plus the few fields only a remote caller needs. * - **Validation is the shared validators.** Exact-key checking is `hasExactKeys`; digests - * are `isReviewSha256Digest`; note size is core's `reviewNoteWithinBounds`, measured once + * are `isReviewSha256Digest`; note size is core's `reviewNoteWithinSizeLimit`, measured once * over the whole note (D1); the resource read request and chunk are core's own types * (D5). This module declares no digest regex, no second byte measurement, and no copy * of a bound that exists elsewhere. diff --git a/test/review-conformance/conformance.test.ts b/test/review-conformance/conformance.test.ts index 1cb16ac59..b4e2081f6 100644 --- a/test/review-conformance/conformance.test.ts +++ b/test/review-conformance/conformance.test.ts @@ -5,7 +5,7 @@ import { type ReviewPublicationAddress, } from "../../src/core/review/generationOrder"; import { isBlankReviewNoteBody, planReviewIntent } from "../../src/core/review/intents"; -import { reviewNoteWithinBounds } from "../../src/core/review/noteBounds"; +import { reviewNoteWithinSizeLimit } from "../../src/core/review/noteSize"; import { createInitialReviewState } from "../../src/core/review/state"; import { createReviewStore } from "../../src/core/review/store"; import { createTestDiffFile } from "../helpers/diff-helpers"; @@ -19,7 +19,7 @@ import { import { REVIEW_CONFORMANCE_FIXTURES } from "./fixtures"; import { REVIEW_NAVIGATION_FIXTURES } from "./navigationFixtures"; import { REVIEW_NOTE_BODY_FIXTURES } from "./noteBodies"; -import { REVIEW_NOTE_BOUNDS_FIXTURES } from "./noteBounds"; +import { REVIEW_NOTE_SIZE_FIXTURES } from "./noteSize"; import { REVIEW_PRODUCER_ORDER_FIXTURES, REVIEW_PUBLICATION_ORDER_FIXTURES, @@ -72,7 +72,7 @@ describe("review conformance corpus", () => { ...REVIEW_PUBLICATION_ORDER_FIXTURES.flatMap((fixture) => fixture.findings), ...REVIEW_PRODUCER_ORDER_FIXTURES.flatMap((fixture) => fixture.findings), ...REVIEW_WIRE_FIXTURES.flatMap((fixture) => fixture.findings), - ...(REVIEW_NOTE_BOUNDS_FIXTURES.length > 0 ? ["D1"] : []), + ...(REVIEW_NOTE_SIZE_FIXTURES.length > 0 ? ["D1"] : []), ]); expect(REQUIRED_FINDINGS.filter((finding) => !covered.has(finding))).toEqual([]); @@ -132,10 +132,10 @@ describe("review conformance: empty note bodies", () => { } }); -describe("review conformance: note bounds", () => { - for (const fixture of REVIEW_NOTE_BOUNDS_FIXTURES) { - test(`${fixture.id} ${fixture.withinBounds ? "fits" : "is too large"}`, () => { - expect(reviewNoteWithinBounds(fixture.build())).toBe(fixture.withinBounds); +describe("review conformance: note size", () => { + for (const fixture of REVIEW_NOTE_SIZE_FIXTURES) { + test(`${fixture.id} ${fixture.withinSizeLimit ? "fits" : "is too large"}`, () => { + expect(reviewNoteWithinSizeLimit(fixture.build())).toBe(fixture.withinSizeLimit); }); } }); @@ -161,9 +161,9 @@ for (const consumer of REVIEW_WIRE_CONSUMERS) { // D1: the note-size corpus is a wire question too. The case every field passes and the // whole note fails must be refused here, before it can be admitted and then poison the // snapshot that publishes it. - for (const fixture of REVIEW_NOTE_BOUNDS_FIXTURES) { - test(`${fixture.id} is ${fixture.withinBounds ? "transportable" : "refused"} (D1)`, () => { - expect(consumer.acceptsNote(fixture.build())).toBe(fixture.withinBounds); + for (const fixture of REVIEW_NOTE_SIZE_FIXTURES) { + test(`${fixture.id} is ${fixture.withinSizeLimit ? "transportable" : "refused"} (D1)`, () => { + expect(consumer.acceptsNote(fixture.build())).toBe(fixture.withinSizeLimit); }); } }); diff --git a/test/review-conformance/consumers/reviewWire.ts b/test/review-conformance/consumers/reviewWire.ts index 732eb9c8f..7a7ac473e 100644 --- a/test/review-conformance/consumers/reviewWire.ts +++ b/test/review-conformance/consumers/reviewWire.ts @@ -7,10 +7,10 @@ * wire type that drifted away from the semantics it carries fails here rather than at a * client (`docs/browser-review-seam-audit.md`, B12/B10). * - * It also runs the note-bounds corpus, because "may this note cross a boundary" is a wire + * It also runs the note-size corpus, because "may this note cross a boundary" is a wire * question the prototype answered differently from the producer (D1). */ -import { reviewNoteWithinBounds } from "../../../src/core/review/noteBounds"; +import { reviewNoteWithinSizeLimit } from "../../../src/core/review/noteSize"; import { parseHunkReviewAction, toReviewIntent } from "../../../src/session/reviewProtocol"; import type { ReviewWireConsumer } from "../types"; @@ -23,5 +23,5 @@ export const reviewWireConsumer: ReviewWireConsumer = { ? { accepted: true, intent: toReviewIntent(parsed.value) } : { accepted: false }; }, - acceptsNote: reviewNoteWithinBounds, + acceptsNote: reviewNoteWithinSizeLimit, }; diff --git a/test/review-conformance/noteBounds.ts b/test/review-conformance/noteSize.ts similarity index 90% rename from test/review-conformance/noteBounds.ts rename to test/review-conformance/noteSize.ts index cae1f017f..2f2851050 100644 --- a/test/review-conformance/noteBounds.ts +++ b/test/review-conformance/noteSize.ts @@ -11,16 +11,16 @@ * Sizes are stated relative to the shared bound rather than as literals, so the corpus * still means the same thing if the bound moves. */ -import { MAX_REVIEW_NOTE_BYTES } from "../../src/core/review/noteBounds"; +import { MAX_REVIEW_NOTE_BYTES } from "../../src/core/review/noteSize"; import type { ReviewNoteV1 } from "../../src/core/review/types"; -export interface ReviewNoteBoundsFixture { +export interface ReviewNoteSizeFixture { id: string; /** What makes this note adversarial, in one line. */ description: string; build: () => ReviewNoteV1; /** Hand-written from the semantics — never captured from the measurement. */ - withinBounds: boolean; + withinSizeLimit: boolean; } /** One minimal note with the given text fields; everything else is framing. */ @@ -42,24 +42,24 @@ const FRAMING_BYTES = JSON.stringify(note({})).length; /** ASCII filler of an exact byte length. */ const filler = (bytes: number) => "x".repeat(Math.max(0, bytes)); -export const REVIEW_NOTE_BOUNDS_FIXTURES: readonly ReviewNoteBoundsFixture[] = [ +export const REVIEW_NOTE_SIZE_FIXTURES: readonly ReviewNoteSizeFixture[] = [ { id: "empty-note", description: "Framing alone is far below the bound.", build: () => note({}), - withinBounds: true, + withinSizeLimit: true, }, { id: "whole-note-exactly-at-the-bound", description: "Summary sized so the serialized note lands on the limit exactly.", build: () => note({ summary: filler(MAX_REVIEW_NOTE_BYTES - FRAMING_BYTES) }), - withinBounds: true, + withinSizeLimit: true, }, { id: "whole-note-one-byte-over", description: "The same note plus one byte: over the limit as a whole.", build: () => note({ summary: filler(MAX_REVIEW_NOTE_BYTES - FRAMING_BYTES + 1) }), - withinBounds: false, + withinSizeLimit: false, }, { id: "every-field-fits-but-the-note-does-not", @@ -71,20 +71,20 @@ export const REVIEW_NOTE_BOUNDS_FIXTURES: readonly ReviewNoteBoundsFixture[] = [ rationale: filler(MAX_REVIEW_NOTE_BYTES - 1), markup: filler(MAX_REVIEW_NOTE_BYTES - 1), }), - withinBounds: false, + withinSizeLimit: false, }, { id: "multibyte-summary-under-the-per-character-limit", description: "A summary of four-byte characters that is well under the bound counted as characters and over it counted as bytes.", build: () => note({ summary: "🧪".repeat(MAX_REVIEW_NOTE_BYTES / 4) }), - withinBounds: false, + withinSizeLimit: false, }, { id: "multibyte-summary-just-inside", description: "The same characters, one short of filling the bound with framing included.", build: () => note({ summary: "🧪".repeat(Math.floor((MAX_REVIEW_NOTE_BYTES - FRAMING_BYTES) / 4)) }), - withinBounds: true, + withinSizeLimit: true, }, ]; diff --git a/test/review-conformance/wireFixtures.ts b/test/review-conformance/wireFixtures.ts index dfb011169..4464c4968 100644 --- a/test/review-conformance/wireFixtures.ts +++ b/test/review-conformance/wireFixtures.ts @@ -10,7 +10,7 @@ * The adversarial cases are the two the audit contributed. B10: a line inside an expanded * gap is addressable at all, because the action carries the proof for it — the prototype's * browser could not express one and had its clicks rejected or mis-sided. D1 is covered by - * the note-bounds corpus, which the wire now runs as a consumer. + * the note-size corpus, which the wire now runs as a consumer. */ import type { ReviewWireFixture } from "./types"; From 0e786617a1bd81c8ae7f431878a6daa966acead4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 17:18:29 +0000 Subject: [PATCH 2/7] refactor(review): move the Node review digest to the tier-neutral lib `src/app/review/digest.ts` called itself "the producer's hashing implementation", but the daemon (`src/session/broker/state.ts`) imports it too, which dragged a broker -> app-tier import edge across the codebase for three lines of Node hashing. `src/lib/` already holds tier-neutral primitives (`sourceText.ts`, `osPath.ts`, `terminalText.ts`), so the digest belongs there and its header now says what it is rather than who owns it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4 --- docs/browser-review-seam-audit.md | 2 +- src/app/review/producer.ts | 2 +- src/{app/review/digest.ts => lib/reviewDigest.ts} | 5 +++-- src/session/broker/state.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) rename src/{app/review/digest.ts => lib/reviewDigest.ts} (83%) diff --git a/docs/browser-review-seam-audit.md b/docs/browser-review-seam-audit.md index 01813b706..4c079faaa 100644 --- a/docs/browser-review-seam-audit.md +++ b/docs/browser-review-seam-audit.md @@ -447,7 +447,7 @@ path suffixes, expansion retention, git-status badges). variant is what let a writer and a reader disagree — with `normalizeReviewDigest` for values arriving from outside and `reviewDigestsEqual` normalizing _both_ operands. Hashing itself is an injected `ReviewDigestFn` rather than inline `createHash` calls; the producer supplies - Node's at the edge (`src/app/review/digest.ts`), which is also what repaid the shared model's + Node's at the edge (`src/lib/reviewDigest.ts`), which is also what repaid the shared model's last node-debt entry. Resource bounds are constants in `core/review/resources.ts` that the producer imports rather than restates. Wire constants, the action-envelope parser, and the two note-filter namings are Phase 3. diff --git a/src/app/review/producer.ts b/src/app/review/producer.ts index cf2623df6..5f3fb3094 100644 --- a/src/app/review/producer.ts +++ b/src/app/review/producer.ts @@ -44,7 +44,7 @@ import { import type { ReviewDigestFn } from "../../core/review/validation"; import type { ReviewStore } from "../../core/review/store"; import type { DiffFile } from "../../core/types"; -import { nodeReviewDigest } from "./digest"; +import { nodeReviewDigest } from "../../lib/reviewDigest"; import { buildReviewPublication, type ReviewPublication } from "./publication"; import { ReviewResourceStore, type ReviewResourceFailure } from "./resourceStore"; diff --git a/src/app/review/digest.ts b/src/lib/reviewDigest.ts similarity index 83% rename from src/app/review/digest.ts rename to src/lib/reviewDigest.ts index 23a15bfdd..cb7032b61 100644 --- a/src/app/review/digest.ts +++ b/src/lib/reviewDigest.ts @@ -1,5 +1,6 @@ /** - * The producer's hashing implementation, kept at the edge. + * The Node implementation of the shared review digest (`ReviewDigestFn`), injected by + * whichever tier owns bytes. * * The shared review model names the algorithm and validates the shape of a digest but * never computes one, so that it stays importable without a hashing runtime. This is the @@ -8,7 +9,7 @@ * browser bundle supply Web Crypto's instead. */ import { createHash } from "node:crypto"; -import { REVIEW_DIGEST_ALGORITHM, type ReviewDigestFn } from "../../core/review/validation"; +import { REVIEW_DIGEST_ALGORITHM, type ReviewDigestFn } from "../core/review/validation"; /** Digest bytes with Node's implementation of the shared algorithm, in canonical form. */ export const nodeReviewDigest: ReviewDigestFn = (bytes) => diff --git a/src/session/broker/state.ts b/src/session/broker/state.ts index 820ec9a17..326d40cd0 100644 --- a/src/session/broker/state.ts +++ b/src/session/broker/state.ts @@ -52,7 +52,7 @@ import { reviewResourceId, type ReviewResourceDescriptorV1, } from "../../core/review/resources"; -import { nodeReviewDigest } from "../../app/review/digest"; +import { nodeReviewDigest } from "../../lib/reviewDigest"; import { HUNK_REVIEW_PROTOCOL_VERSION, type HunkReviewActionResultV1, From 45bc8ed6d711a2fa412dcc2b18c28be0eb70f130 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 17:19:03 +0000 Subject: [PATCH 3/7] refactor(review): name the producer's resource budget for its tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MAX_REVIEW_RESOURCE_CACHE_BYTES` and `MAX_REVIEW_DAEMON_CACHE_BYTES` are identical 64 MB budgets living in two tiers, and only the daemon's said which tier it belonged to — so the producer's read like the resource budget rather than one of two. Rename it to `MAX_REVIEW_PRODUCER_RESOURCE_BYTES` so the pair is greppable and each name states its own side. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4 --- src/app/review/resourceStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/review/resourceStore.ts b/src/app/review/resourceStore.ts index 0f7b7df91..ee34e1f33 100644 --- a/src/app/review/resourceStore.ts +++ b/src/app/review/resourceStore.ts @@ -41,7 +41,7 @@ import { } from "./publication"; /** How many materialized bytes one generation retains before evicting its oldest. */ -export const MAX_REVIEW_RESOURCE_CACHE_BYTES = 64 * 1024 * 1024; +export const MAX_REVIEW_PRODUCER_RESOURCE_BYTES = 64 * 1024 * 1024; export interface MaterializedReviewResource { bytes: Uint8Array; @@ -93,7 +93,7 @@ export class ReviewResourceStore { publication, digest, concurrency = REVIEW_RESOURCE_LOAD_CONCURRENCY, - maxCacheBytes = MAX_REVIEW_RESOURCE_CACHE_BYTES, + maxCacheBytes = MAX_REVIEW_PRODUCER_RESOURCE_BYTES, }: ReviewResourceStoreOptions) { this.publication = publication; this.digest = digest; From 0b1545af0e77aa9acec4bf1375229ebaa756b107 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 17:20:13 +0000 Subject: [PATCH 4/7] test(review): name the geometry conformance registry for what it covers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test/review-conformance/` runs four peer registries — geometry, navigation, ordering, wire — and three are named for the question they answer. The first wore the harness's generic name (`REVIEW_CONFORMANCE_CONSUMERS`, `REVIEW_CONFORMANCE_FIXTURES`, `fixtures.ts`), so a future consumer would read it as "the" registry and join the wrong one. The doc comments already call them the geometry consumers; the code now says so too, types included (`ReviewGeometryConsumer`/`Fixture`/`Projection`). The `Conformance*` shapes stay as they are — they are shared harness vocabulary, not registry names. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4 --- docs/browser-review-seam-audit.md | 2 +- test/review-conformance/conformance.test.ts | 12 ++++++------ test/review-conformance/consumers.ts | 4 ++-- test/review-conformance/consumers/coreModel.ts | 8 ++++---- test/review-conformance/consumers/reviewProducer.ts | 8 ++++---- .../consumers/terminalRenderPlan.ts | 8 ++++---- .../{fixtures.ts => geometryFixtures.ts} | 4 ++-- test/review-conformance/types.ts | 10 +++++----- 8 files changed, 28 insertions(+), 28 deletions(-) rename test/review-conformance/{fixtures.ts => geometryFixtures.ts} (98%) diff --git a/docs/browser-review-seam-audit.md b/docs/browser-review-seam-audit.md index 4c079faaa..ff30e6f70 100644 --- a/docs/browser-review-seam-audit.md +++ b/docs/browser-review-seam-audit.md @@ -50,7 +50,7 @@ draft-body intent yet, recorded under B12. Fix: terminal calls `reviewGapAddress`; delete its local math. _Repaid (Phase 1 PR 2)_: `reviewLeadingGap`/`reviewGapAddress` in `core/review/expansion.ts`; `pierre.ts` copies deleted; fixtures `pure-insertion-hunk` and `pure-deletion-hunk` in - `test/review-conformance/fixtures.ts`; core and terminal render planning both registered. + `test/review-conformance/geometryFixtures.ts`; core and terminal render planning both registered. Residual (found in review): when the anchor side has zero rows and untouched content precedes the hunk, the parser's `collapsedBefore` undercounts the leading gap by one line — the leading-side sibling of A2's residual, recorded on `reviewLeadingGap` and pinned diff --git a/test/review-conformance/conformance.test.ts b/test/review-conformance/conformance.test.ts index b4e2081f6..f40e7dfde 100644 --- a/test/review-conformance/conformance.test.ts +++ b/test/review-conformance/conformance.test.ts @@ -11,12 +11,12 @@ import { createReviewStore } from "../../src/core/review/store"; import { createTestDiffFile } from "../helpers/diff-helpers"; import { createTestReviewDocument } from "../helpers/review-store-helpers"; import { - REVIEW_CONFORMANCE_CONSUMERS, + REVIEW_GEOMETRY_CONSUMERS, REVIEW_NAVIGATION_CONSUMERS, REVIEW_ORDERING_CONSUMERS, REVIEW_WIRE_CONSUMERS, } from "./consumers"; -import { REVIEW_CONFORMANCE_FIXTURES } from "./fixtures"; +import { REVIEW_GEOMETRY_FIXTURES } from "./geometryFixtures"; import { REVIEW_NAVIGATION_FIXTURES } from "./navigationFixtures"; import { REVIEW_NOTE_BODY_FIXTURES } from "./noteBodies"; import { REVIEW_NOTE_SIZE_FIXTURES } from "./noteSize"; @@ -47,7 +47,7 @@ const REQUIRED_FINDINGS = [ describe("review conformance corpus", () => { test("registers every consumer that has landed so far", () => { - expect(REVIEW_CONFORMANCE_CONSUMERS.map((consumer) => consumer.name)).toEqual([ + expect(REVIEW_GEOMETRY_CONSUMERS.map((consumer) => consumer.name)).toEqual([ "core review model", "terminal render planning", "review producer", @@ -67,7 +67,7 @@ describe("review conformance corpus", () => { test("carries an adversarial fixture for every finding it claims to repay", () => { const covered = new Set([ - ...REVIEW_CONFORMANCE_FIXTURES.flatMap((fixture) => fixture.findings), + ...REVIEW_GEOMETRY_FIXTURES.flatMap((fixture) => fixture.findings), ...REVIEW_NAVIGATION_FIXTURES.flatMap((fixture) => fixture.findings), ...REVIEW_PUBLICATION_ORDER_FIXTURES.flatMap((fixture) => fixture.findings), ...REVIEW_PRODUCER_ORDER_FIXTURES.flatMap((fixture) => fixture.findings), @@ -89,9 +89,9 @@ for (const consumer of REVIEW_NAVIGATION_CONSUMERS) { }); } -for (const consumer of REVIEW_CONFORMANCE_CONSUMERS) { +for (const consumer of REVIEW_GEOMETRY_CONSUMERS) { describe(`review conformance: ${consumer.name}`, () => { - for (const fixture of REVIEW_CONFORMANCE_FIXTURES) { + for (const fixture of REVIEW_GEOMETRY_FIXTURES) { test(`${fixture.id} (${fixture.findings.join(", ")})`, () => { expect(consumer.project(fixture)).toEqual(fixture.expected); }); diff --git a/test/review-conformance/consumers.ts b/test/review-conformance/consumers.ts index 940cf4fdc..d83e0b5a7 100644 --- a/test/review-conformance/consumers.ts +++ b/test/review-conformance/consumers.ts @@ -15,13 +15,13 @@ import { reviewWireConsumer } from "./consumers/reviewWire"; import { terminalReviewControllerNavigationConsumer } from "./consumers/terminalReviewController"; import { terminalRenderPlanConsumer } from "./consumers/terminalRenderPlan"; import type { - ReviewConformanceConsumer, + ReviewGeometryConsumer, ReviewNavigationConsumer, ReviewOrderingConsumer, ReviewWireConsumer, } from "./types"; -export const REVIEW_CONFORMANCE_CONSUMERS: readonly ReviewConformanceConsumer[] = [ +export const REVIEW_GEOMETRY_CONSUMERS: readonly ReviewGeometryConsumer[] = [ coreModelConsumer, terminalRenderPlanConsumer, reviewProducerConsumer, diff --git a/test/review-conformance/consumers/coreModel.ts b/test/review-conformance/consumers/coreModel.ts index 70a08a049..600ccfe02 100644 --- a/test/review-conformance/consumers/coreModel.ts +++ b/test/review-conformance/consumers/coreModel.ts @@ -25,8 +25,8 @@ import type { ConformanceExpandedRow, ConformanceGap, ConformanceExpansion, - ReviewConformanceConsumer, - ReviewConformanceFixture, + ReviewGeometryConsumer, + ReviewGeometryFixture, } from "../types"; /** Collect one file's gaps in the order a top-to-bottom renderer meets them. */ @@ -78,10 +78,10 @@ function expandedRowsOf( })); } -export const coreModelConsumer: ReviewConformanceConsumer = { +export const coreModelConsumer: ReviewGeometryConsumer = { name: "core review model", phase: "Phase 1 PR 2", - project(fixture: ReviewConformanceFixture) { + project(fixture: ReviewGeometryFixture) { const document = projectReviewDocument(fixture.build()); return { files: document.files.map((file, fileIndex) => { diff --git a/test/review-conformance/consumers/reviewProducer.ts b/test/review-conformance/consumers/reviewProducer.ts index f104825c8..b66f90496 100644 --- a/test/review-conformance/consumers/reviewProducer.ts +++ b/test/review-conformance/consumers/reviewProducer.ts @@ -26,8 +26,8 @@ import { createReviewStore } from "../../../src/core/review/store"; import type { ConformanceExpandedRow, ConformanceGap, - ReviewConformanceConsumer, - ReviewConformanceFixture, + ReviewGeometryConsumer, + ReviewGeometryFixture, } from "../types"; /** Report one manifest gap in the shape the corpus states gaps in. */ @@ -62,10 +62,10 @@ function expandedRowsOf( })); } -export const reviewProducerConsumer: ReviewConformanceConsumer = { +export const reviewProducerConsumer: ReviewGeometryConsumer = { name: "review producer", phase: "Phase 2", - project(fixture: ReviewConformanceFixture) { + project(fixture: ReviewGeometryFixture) { const files = fixture.build(); const producer = new ReviewProducer( { files, sourceLabel: "conformance" }, diff --git a/test/review-conformance/consumers/terminalRenderPlan.ts b/test/review-conformance/consumers/terminalRenderPlan.ts index 1e7024ae9..49123ac82 100644 --- a/test/review-conformance/consumers/terminalRenderPlan.ts +++ b/test/review-conformance/consumers/terminalRenderPlan.ts @@ -19,8 +19,8 @@ import type { ConformanceExpandedRow, ConformanceGap, ConformanceHunkRanges, - ReviewConformanceConsumer, - ReviewConformanceFixture, + ReviewGeometryConsumer, + ReviewGeometryFixture, } from "../types"; const THEME = resolveTheme("github-dark-default", null); @@ -87,10 +87,10 @@ function hunkRangesOf(file: DiffFile): ConformanceHunkRanges[] { }); } -export const terminalRenderPlanConsumer: ReviewConformanceConsumer = { +export const terminalRenderPlanConsumer: ReviewGeometryConsumer = { name: "terminal render planning", phase: "Phase 1 PR 2", - project(fixture: ReviewConformanceFixture) { + project(fixture: ReviewGeometryFixture) { const files = fixture.build(); return { files: files.map((file, fileIndex) => { diff --git a/test/review-conformance/fixtures.ts b/test/review-conformance/geometryFixtures.ts similarity index 98% rename from test/review-conformance/fixtures.ts rename to test/review-conformance/geometryFixtures.ts index 4860fff85..93a7c0530 100644 --- a/test/review-conformance/fixtures.ts +++ b/test/review-conformance/geometryFixtures.ts @@ -8,7 +8,7 @@ */ import { createTestDiffFile, lines } from "../helpers/diff-helpers"; import type { DiffFile } from "../../src/core/types"; -import type { ReviewConformanceFixture } from "./types"; +import type { ReviewGeometryFixture } from "./types"; /** Twelve numbered lines, the base every geometry fixture edits. */ const BASE_LINES = Array.from({ length: 12 }, (_unused, index) => `line ${index + 1}`); @@ -47,7 +47,7 @@ const CHANGED_SIXTH_AFTER = edited((values) => values.map((l) => (l === "line 6" ? "line six" : l)), ); -export const REVIEW_CONFORMANCE_FIXTURES: readonly ReviewConformanceFixture[] = [ +export const REVIEW_GEOMETRY_FIXTURES: readonly ReviewGeometryFixture[] = [ { id: "pure-insertion-hunk", findings: ["A1", "A2", "A10"], diff --git a/test/review-conformance/types.ts b/test/review-conformance/types.ts index 9e5dcb4f5..08a3447f5 100644 --- a/test/review-conformance/types.ts +++ b/test/review-conformance/types.ts @@ -63,7 +63,7 @@ export interface ConformanceFileProjection { expandedRows?: ConformanceExpandedRow[]; } -export interface ReviewConformanceProjection { +export interface ReviewGeometryProjection { files: ConformanceFileProjection[]; } @@ -75,7 +75,7 @@ export interface ConformanceExpansion { sourceText: string; } -export interface ReviewConformanceFixture { +export interface ReviewGeometryFixture { id: string; /** Audit finding ids this fixture guards, e.g. `A1`. */ findings: string[]; @@ -84,7 +84,7 @@ export interface ReviewConformanceFixture { build: () => DiffFile[]; expansion?: ConformanceExpansion; /** Hand-written from the semantics — never captured from a primitive. */ - expected: ReviewConformanceProjection; + expected: ReviewGeometryProjection; } /** @@ -94,11 +94,11 @@ export interface ReviewConformanceFixture { * calling core directly — the terminal adapter drives row building, a producer adapter * drives publication, a browser adapter drives its own projection. */ -export interface ReviewConformanceConsumer { +export interface ReviewGeometryConsumer { name: string; /** The phase that registered this consumer, for the gate ladder's records. */ phase: string; - project: (fixture: ReviewConformanceFixture) => ReviewConformanceProjection; + project: (fixture: ReviewGeometryFixture) => ReviewGeometryProjection; } /** From c3442b2b005ffcdccf3d8fd7df3763ed6511f8b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 17:20:37 +0000 Subject: [PATCH 5/7] docs(review): point publication.ts at the generation-ordering vocabulary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header already sends a reader to `core/review/document.ts` for the document it publishes, but said nothing about where a generation's address and ordering rules live — so `generationOrder.ts` was reachable only by grep. One sentence, following the existing pattern. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4 --- src/app/review/publication.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/review/publication.ts b/src/app/review/publication.ts index cabc33cab..292d93ba5 100644 --- a/src/app/review/publication.ts +++ b/src/app/review/publication.ts @@ -9,7 +9,8 @@ * * The document itself deliberately knows nothing about any of this * (`src/core/review/document.ts`); publication is layered on top so the shared model stays - * a description of a review rather than of a transport. + * a description of a review rather than of a transport. The vocabulary for addressing and + * ordering those generations is `src/core/review/generationOrder.ts`. */ import { buildReviewContentManifest, From 0b166195a1bf27a7ff9f95986b85a4ea664e35a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 17:28:30 +0000 Subject: [PATCH 6/7] chore: add an empty changeset for the review naming cleanup Rename-only maintenance with no user-visible behavior change, so it should not appear in release notes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4 --- .changeset/review-cleanup-renames.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/review-cleanup-renames.md diff --git a/.changeset/review-cleanup-renames.md b/.changeset/review-cleanup-renames.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/review-cleanup-renames.md @@ -0,0 +1,2 @@ +--- +--- From 3f753ce80454baa73c41cd23c14ff4c59ca550df Mon Sep 17 00:00:00 2001 From: Ben Vinegar <2153+benvinegar@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:46:12 -0400 Subject: [PATCH 7/7] =?UTF-8?q?docs(review):=20one=20telling=20per=20invar?= =?UTF-8?q?iant=20=E2=80=94=20trim=20the=20rebuilt=20modules'=20header=20a?= =?UTF-8?q?utopsies=20(#739)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude --- .changeset/review-header-cleanup.md | 2 ++ src/app/review/producer.ts | 20 ++++++++-------- src/app/review/publication.ts | 8 +++---- src/app/review/resourceStore.ts | 2 +- src/core/commandCatalog.ts | 14 +++++------ src/core/review/actions.ts | 2 +- src/core/review/address.ts | 7 ++++++ src/core/review/anchors.ts | 12 ++++++---- src/core/review/annotations.ts | 6 ++--- src/core/review/canonicalFile.ts | 6 +---- src/core/review/contentManifest.ts | 10 ++++---- src/core/review/document.ts | 5 ++-- src/core/review/generationOrder.ts | 12 +++++----- src/core/review/geometry.ts | 6 ++--- src/core/review/identity.ts | 8 +++---- src/core/review/intents.ts | 23 ++++++++++++++----- src/core/review/navigation.ts | 8 +++---- src/core/review/noteSize.ts | 20 +++++----------- src/core/review/reducer.ts | 6 ++--- src/core/review/resourceAssembly.ts | 6 +++++ src/core/review/state.ts | 5 ++++ src/core/review/types.ts | 4 ++-- src/core/review/validation.ts | 12 +++++----- src/session/broker/reviewMirror.ts | 13 ++++------- src/session/reviewProtocol.ts | 5 ++-- .../consumers/brokerMirror.ts | 7 ++---- .../consumers/reviewProducer.ts | 3 +-- .../consumers/reviewWire.ts | 2 +- test/review-conformance/noteSize.ts | 10 ++++---- test/review-conformance/orderingFixtures.ts | 9 +++----- test/review-conformance/types.ts | 6 ++--- test/review-conformance/wireFixtures.ts | 8 +++---- 32 files changed, 135 insertions(+), 132 deletions(-) create mode 100644 .changeset/review-header-cleanup.md diff --git a/.changeset/review-header-cleanup.md b/.changeset/review-header-cleanup.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/review-header-cleanup.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/src/app/review/producer.ts b/src/app/review/producer.ts index 5f3fb3094..88c65b220 100644 --- a/src/app/review/producer.ts +++ b/src/app/review/producer.ts @@ -8,16 +8,14 @@ * against the shared contract (`core/review/generationOrder.ts`) before anything is * published, so a producer bug fails here rather than desynchronizing a reader. * - * It deliberately does *not* own the review's live state. The terminal's controller owns - * the store today, and moving that is a behavior change rather than a seam extraction; the - * producer attaches to whichever store the host mounted and plans intents against it, - * supplying the caller-owned facts core refuses to invent — identity, time, and the - * annotation index (`ReviewIntentFacts.annotations`) — through the same derivation the - * terminal uses. + * It does *not* own the review's live state. The terminal's controller owns the store + * today, and moving that is a behavior change rather than a seam extraction; the producer + * attaches to whichever store the host mounted and plans intents against it, supplying the + * caller-owned facts core refuses to invent (identity, time, and the annotation index, + * `ReviewIntentFacts.annotations`) through the same derivation the terminal uses. * * No transport lives here. Serving the session surface means answering method calls; HTTP, - * SSE, and a browser client are later phases, and their absence is what keeps this module - * about the review rather than about a protocol. + * SSE, and a browser client are later phases. */ import { assertReviewPublicationAdvance, @@ -158,9 +156,9 @@ export class ReviewProducer { /** * The review state this producer plans against, when a host has attached one. * - * Read-only, and deliberately the *store's* state rather than a copy: a caller - * validating a request against the current review — does this file exist, is this the - * draft I opened — must see exactly what the next intent will be planned against. + * Read-only, and the *store's* state rather than a copy: a caller validating a request + * against the current review — does this file exist, is this the draft I opened — must + * see exactly what the next intent will be planned against. */ getReviewState() { return this.store?.getSnapshot(); diff --git a/src/app/review/publication.ts b/src/app/review/publication.ts index 292d93ba5..515d07d7a 100644 --- a/src/app/review/publication.ts +++ b/src/app/review/publication.ts @@ -7,10 +7,10 @@ * mutating this one — which is what makes "which generation is this?" answerable rather * than a matter of timing. * - * The document itself deliberately knows nothing about any of this - * (`src/core/review/document.ts`); publication is layered on top so the shared model stays - * a description of a review rather than of a transport. The vocabulary for addressing and - * ordering those generations is `src/core/review/generationOrder.ts`. + * The document itself knows nothing about any of this (`src/core/review/document.ts`); + * publication is layered on top so the shared model stays a description of a review rather + * than of a transport. The vocabulary for addressing and ordering those generations is + * `src/core/review/generationOrder.ts`. */ import { buildReviewContentManifest, diff --git a/src/app/review/resourceStore.ts b/src/app/review/resourceStore.ts index ee34e1f33..236d95861 100644 --- a/src/app/review/resourceStore.ts +++ b/src/app/review/resourceStore.ts @@ -1,7 +1,7 @@ /** * Materializing and serving one generation's resources. * - * Three rules shape this module, each of them a defect the prototype shipped: + * Three rules shape this module: * * - **Single flight per resource.** A resource is produced at most once per generation, * and concurrent readers share that one production. It is not a cache bolted on top: a diff --git a/src/core/commandCatalog.ts b/src/core/commandCatalog.ts index 91d0cc015..2eaa7ffcc 100644 --- a/src/core/commandCatalog.ts +++ b/src/core/commandCatalog.ts @@ -13,17 +13,17 @@ * attached surface sees the result. Its effect is declared as data rather than as a * function, which is what lets the same declaration drive the terminal's handler, an * agent command, and later a wire action. - * - `client-local` — deliberately per-client view state (scrolling, layout, theme, help). + * - `client-local` — per-client view state (scrolling, layout, theme, help). * Each client implements its own handler; sharing identity is what keeps help screens * and palettes agreeing about what the command is called and what it is bound to. * - `host-only` — it runs where the review is hosted (quitting, reloading the source, - * opening `$EDITOR`). Not invocable from a remote client without an explicit allowlist, - * which is a scope boundary rather than a missing feature (audit F4). + * opening `$EDITOR`). Not invocable from a remote client without an explicit allowlist + * (audit F4). * - * This module is deliberately renderer-neutral and dependency-light: no OpenTUI, no React, - * no Node builtins, chords as plain strings. It is not part of `src/core/review` because - * it describes UI vocabulary rather than review semantics, and that module stays purely - * about what a review *is*. + * This module is renderer-neutral and dependency-light: no OpenTUI, no React, no Node + * builtins, chords as plain strings. It is not part of `src/core/review` because it + * describes UI vocabulary rather than review semantics, and that module stays purely about + * what a review *is*. */ import type { ReviewIntent } from "./review/intents"; import type { ReviewSelectionScope } from "./review/navigation"; diff --git a/src/core/review/actions.ts b/src/core/review/actions.ts index 830b35fc5..cb2345a75 100644 --- a/src/core/review/actions.ts +++ b/src/core/review/actions.ts @@ -1,5 +1,5 @@ /** - * Declares the actions the reducer executes — decided state transitions only. + * Declares the actions the reducer executes: decided state transitions only. * * An action states what changes, not whether it should: lifecycle code and intent plans * decide that first. `reduceReviewState` applies one without further validation beyond diff --git a/src/core/review/address.ts b/src/core/review/address.ts index a0a65fd1f..9df0ebd95 100644 --- a/src/core/review/address.ts +++ b/src/core/review/address.ts @@ -16,6 +16,13 @@ */ import type { ReviewSide } from "./types"; +/** + * One addressable thing in a review, at the granularity the caller means. + * + * `file` and `hunk` name structure; `line` names a position in the content by side, which + * exists whether or not anything is anchored there; `note` names one anchored note by its + * id, which outlives the line it currently hangs from. + */ export type ReviewAddress = | { kind: "file"; fileKey: string } | { kind: "hunk"; fileKey: string; hunkIndex: number } diff --git a/src/core/review/anchors.ts b/src/core/review/anchors.ts index d06303b94..ddfd09332 100644 --- a/src/core/review/anchors.ts +++ b/src/core/review/anchors.ts @@ -6,10 +6,10 @@ * this resolver placed through its fallback is not silently dropped by a consumer that * re-derives placement (`docs/browser-review-seam-audit.md`, D3/B8). * - * Resolution is deliberately permissive: an imported note may name a line the current - * patch no longer shows, and losing it entirely would be worse than hanging it from the - * nearest real hunk. Callers that must reject an unbacked target validate before - * anchoring rather than reading a verdict out of the anchor. + * Resolution is permissive: an imported note may name a line the current patch no longer + * shows, and losing it entirely would be worse than hanging it from the nearest real hunk. + * Callers that must reject an unbacked target validate before anchoring rather than + * reading a verdict out of the anchor. */ import { reviewHunkRange, reviewRangesOverlap, type ReviewHunkSpan } from "./geometry"; import type { ReviewLineRange, ReviewRangeAnchorV1, ReviewSide } from "./types"; @@ -17,6 +17,10 @@ import type { ReviewLineRange, ReviewRangeAnchorV1, ReviewSide } from "./types"; export interface ReviewNoteAnchorInput { oldRange?: ReviewLineRange; newRange?: ReviewLineRange; + /** + * The line the note was placed on. When it lands inside a hunk it decides ownership, + * ahead of any range that also intersects one. + */ preferred?: { side: ReviewSide; line: number }; /** * The hunk that owns the note when no range intersects one — an expanded-gap line, or diff --git a/src/core/review/annotations.ts b/src/core/review/annotations.ts index ac3c62504..622508154 100644 --- a/src/core/review/annotations.ts +++ b/src/core/review/annotations.ts @@ -5,12 +5,12 @@ * index, and core cannot compute it: notes arrive from sources the semantic document does * not carry (a sidecar loaded with the changeset, live agent comments, the reviewer's own * notes), and only the consumer that merged them onto the diff model knows the full set. - * It is therefore a caller-supplied fact (`ReviewIntentFacts.annotations`) — but the + * It is therefore a caller-supplied fact (`ReviewIntentFacts.annotations`), but the * *derivation* is shared, so the terminal and the producer hand the planner the same * answer instead of two that agree by coincidence. * - * File membership is deliberately broader than hunk membership: a file carrying review - * context but no note inside any hunk is still a stop on the annotated-file tour. + * File membership is broader than hunk membership: a file carrying review context but no + * note inside any hunk is still a stop on the annotated-file tour. */ import { reviewHunkRanges, reviewRangesOverlap, type ReviewHunkSpan } from "./geometry"; import type { ReviewAnnotationIndex } from "./navigation"; diff --git a/src/core/review/canonicalFile.ts b/src/core/review/canonicalFile.ts index 7b461ae54..92ddc2fc1 100644 --- a/src/core/review/canonicalFile.ts +++ b/src/core/review/canonicalFile.ts @@ -2,11 +2,7 @@ * Does this serialized file still describe the review it came from? * * A producer serves each reviewed file as a canonical JSON resource, and a reader has to - * be able to check that what it received matches the review it was published with. The - * prototype checked this three times with three different field lists — seventeen fields - * at the producer, twelve in the browser, ten in the broker — and the browser's compared - * two of them by `JSON.stringify`, so a lazily inserted key could spuriously fail a file - * that had not changed at all. None of the three compared hunk *content* + * be able to check that what it received matches the review it was published with * (`docs/browser-review-seam-audit.md`, D4). * * There is one check here, and it does not carry a field list of its own: it projects the diff --git a/src/core/review/contentManifest.ts b/src/core/review/contentManifest.ts index 36a326eb3..172c519d0 100644 --- a/src/core/review/contentManifest.ts +++ b/src/core/review/contentManifest.ts @@ -1,15 +1,13 @@ /** * A deterministic semantic snapshot of one review document. * - * The manifest is a parity instrument, not a validator: every consumer of the shared - * model can be driven through the same fixture and compared against the same manifest, - * so a renderer that re-derives geometry instead of consuming core fails visibly rather - * than drifting quietly. + * The manifest exists so every consumer of the shared model can be driven through the same + * fixture and compared against the same snapshot, making a renderer that re-derives + * geometry instead of consuming core fail visibly rather than drift quietly. * * It therefore records *derived* geometry — hunk extents, gap addresses, default note * targets, the reason a file renders nothing — alongside the content those derivations - * read. Renderer identity (runtime ids, rows, widths) is deliberately absent, since two - * consumers agreeing on it would prove nothing. + * read. Renderer identity (runtime ids, rows, widths) is left out. */ import { reviewExpansionSide, diff --git a/src/core/review/document.ts b/src/core/review/document.ts index 31f1a82ca..29dc87743 100644 --- a/src/core/review/document.ts +++ b/src/core/review/document.ts @@ -7,9 +7,8 @@ * the store, the terminal's note projection, later a transport — reads the result rather * than the diff model behind it. * - * Publication concerns (generations, resource descriptors, byte digests) deliberately do - * not appear: they belong to the producer runtime that serves a document, not to the - * document itself. + * Publication concerns (generations, resource descriptors, byte digests) do not appear: + * they belong to the producer runtime that serves a document, not to the document itself. */ import type { DiffFile } from "../types"; import { diff --git a/src/core/review/generationOrder.ts b/src/core/review/generationOrder.ts index 0b693421e..90e914b4b 100644 --- a/src/core/review/generationOrder.ts +++ b/src/core/review/generationOrder.ts @@ -19,14 +19,14 @@ * - Within one generation, state revisions strictly increase but need **not** be * contiguous: a receiver that joined late, replayed a log, or took a fresh snapshot * legitimately sees jumps. A revision that repeats is a replay, not an update. - * - Across generations, revisions are not comparable at all — a new generation may restart - * them — which is why a generation change is classified as its own verdict rather than - * folded into revision comparison. + * - Across generations, revisions are not comparable at all, since a new generation may + * restart them. A generation change is therefore classified as its own verdict rather + * than folded into revision comparison. * * Non-semantic republication (a renderer width changed, nothing about the review did) is - * deliberately *not* modelled here. It carries no new position, so it classifies as a - * replay; whoever needs to re-emit it decides that on its own publication key rather than - * by loosening this comparison. + * *not* modelled here. It carries no new position, so it classifies as a replay; whoever + * needs to re-emit it decides that on its own publication key rather than by loosening + * this comparison. */ export interface ReviewGenerationIdentity { diff --git a/src/core/review/geometry.ts b/src/core/review/geometry.ts index 5b49da746..2f2f06831 100644 --- a/src/core/review/geometry.ts +++ b/src/core/review/geometry.ts @@ -7,9 +7,9 @@ * once, because a renderer that re-derives one silently disagrees with the state store * that validates against it (`docs/browser-review-seam-audit.md`, A3/A4/A6/A10). * - * The inputs are deliberately structural rather than `ReviewFileV1`: a parsed diff hunk - * and a projected `ReviewHunkV1` both satisfy them, so the terminal can call these - * primitives from its render path without projecting a whole semantic document first. + * The inputs are structural rather than `ReviewFileV1`: a parsed diff hunk and a projected + * `ReviewHunkV1` both satisfy them, so the terminal can call these primitives from its + * render path without projecting a whole semantic document first. */ import type { ReviewLineAddressV1, ReviewLineRange, ReviewSide } from "./types"; diff --git a/src/core/review/identity.ts b/src/core/review/identity.ts index 5dd3fbf48..e7f83e40d 100644 --- a/src/core/review/identity.ts +++ b/src/core/review/identity.ts @@ -6,10 +6,10 @@ * positions or renderer object identity. Every identity here is a pure function of the * facts it names, so two processes projecting the same content agree. * - * The digest is an identity hash, not an integrity check: it is deliberately - * platform-neutral arithmetic rather than a crypto primitive, so the shared model stays - * importable from a browser bundle without a hashing runtime. Wire-integrity digests - * belong beside the transport that verifies bytes, not here. + * The digest is an identity hash, not an integrity check: it is platform-neutral + * arithmetic rather than a crypto primitive, so the shared model stays importable from a + * browser bundle without a hashing runtime. Wire-integrity digests belong beside the + * transport that verifies bytes, not here. */ /** Four independent 32-bit FNV-1a lanes, giving a 128-bit identity from one pass. */ diff --git a/src/core/review/intents.ts b/src/core/review/intents.ts index 30e988003..26e38b6f7 100644 --- a/src/core/review/intents.ts +++ b/src/core/review/intents.ts @@ -38,6 +38,13 @@ import { import type { ReviewStore } from "./store"; import type { ReviewFileV1, ReviewLineAddressV1, ReviewLineRange, ReviewSide } from "./types"; +/** + * The facts core refuses to invent, supplied by whoever submits an intent. + * + * Identity, time, and the annotation index all depend on the runtime a review is hosted + * in, so planning reads them from here instead of reaching for a clock, a UUID source, or + * a note set the semantic document does not carry. + */ export interface ReviewIntentFacts { /** Caller-allocated identity for a newly persisted note. */ noteId?: string; @@ -62,6 +69,7 @@ export interface ReviewIntentFacts { } export type ReviewIntent = + /** Select one hunk outright, revealing it the way the caller asks. */ | { type: "selection/select"; fileKey: string; hunkIndex: number; reveal: ReviewRevealRequest } /** Step the selection through one navigable scope; the scope decides wrap and reveal. */ | { type: "selection/move"; scope: ReviewSelectionScope; delta: number } @@ -69,7 +77,9 @@ export type ReviewIntent = | { type: "selection/select-file"; fileKey: string; reveal?: ReviewRevealRequest } /** Adopt the position a renderer's viewport settled on, without moving any viewport. */ | { type: "selection/anchor"; fileKey: string; hunkIndex: number } + /** Replace the review's file filter, which decides the visible stream. */ | { type: "filter/set"; filter: string } + /** Set whether agent notes are shown; reviewer-authored notes stay visible either way. */ | { type: "notes/set-visibility"; visible: boolean } /** Open a draft at one hunk, defaulting to the line a whole-hunk note hangs from. */ | { @@ -81,7 +91,9 @@ export type ReviewIntent = } /** Persist the active draft; a blank body retires the draft instead. */ | { type: "notes/create-user"; consumeDraft: true } + /** Delete one reviewer-authored note by id. */ | { type: "notes/remove-user"; noteId: string } + /** Dismiss one live agent note by id, leaving reviewer notes untouched. */ | { type: "notes/remove-live"; noteId: string } | { type: "notes/clear"; fileKey?: string; includeUser?: boolean } /** Flip one addressable collapsed gap between collapsed and expanded. */ @@ -90,12 +102,11 @@ export type ReviewIntent = /** * Every intent type, as a value rather than only as a type. * - * The wire vocabulary is derived from this list instead of restated beside it: the - * prototype hand-copied the action union into three more places, so an intent added to - * one was silently unreachable from the others (`docs/browser-review-seam-audit.md`, - * B12). The assertion below makes the list total — adding a member to `ReviewIntent` - * without naming it here fails to typecheck — and `src/session/reviewProtocol.ts` - * subtracts a named exclusion list from it rather than writing its own. + * The wire vocabulary is derived from this list instead of restated beside it + * (`docs/browser-review-seam-audit.md`, B12). The assertion below makes the list total — + * adding a member to `ReviewIntent` without naming it here fails to typecheck — and + * `src/session/reviewProtocol.ts` subtracts a named exclusion list from it rather than + * writing its own. */ export const REVIEW_INTENT_TYPES = [ "selection/select", diff --git a/src/core/review/navigation.ts b/src/core/review/navigation.ts index 8cfe4e544..0c32934f9 100644 --- a/src/core/review/navigation.ts +++ b/src/core/review/navigation.ts @@ -18,10 +18,10 @@ * another file puts that file's header on screen; crossing backward reveals the hunk * itself; annotated-hunk navigation asks for the note. Callers do not re-decide this. * - * The model this plans over is deliberately structural — file keys and hunk counts, plus - * an annotation index the consumer supplies. Which hunks count as annotated depends on - * note sources the semantic document does not carry (an imported sidecar, a renderer's - * merged live comments), so it arrives as a caller-owned fact rather than being guessed. + * The model this plans over is structural — file keys and hunk counts, plus an annotation + * index the consumer supplies. Which hunks count as annotated depends on note sources the + * semantic document does not carry (an imported sidecar, a renderer's merged live + * comments), so it arrives as a caller-owned fact rather than being guessed. */ import type { ReviewRevealRequest, ReviewSemanticSelection } from "./state"; diff --git a/src/core/review/noteSize.ts b/src/core/review/noteSize.ts index 76910ce3d..a33e3c939 100644 --- a/src/core/review/noteSize.ts +++ b/src/core/review/noteSize.ts @@ -1,16 +1,9 @@ /** * The one size a review note is measured against. * - * The prototype measured notes in two units: action validation checked `body` and - * `markup` separately, while the producer and broker checked the whole serialized note. - * A note could therefore pass the check that admitted it and then fail the check that - * published it — poisoning an entire snapshot with a capacity error rather than rejecting - * one note (`docs/browser-review-seam-audit.md`, D1). - * - * So there is one measurement, and it is the whole note: everything that will be - * serialized, counted together, in the unit the transport actually pays. A composer - * checking a note it is about to create and a producer checking a note it is about to - * publish call the same function and get the same answer. + * A note is measured whole — its serialized JSON, including framing — never field by + * field, so the check that admits a note and the check that publishes it cannot disagree. + * Composers and producers call the same function (`docs/browser-review-seam-audit.md`, D1). */ import type { ReviewNoteV1 } from "./types"; import { utf8ByteLength } from "./validation"; @@ -21,10 +14,9 @@ export const MAX_REVIEW_NOTE_BYTES = 256 * 1024; /** * The serialized size of one note. * - * Measured over the note's JSON form, because that is what a snapshot carries — summing - * the text fields alone would undercount the framing every one of them is wrapped in, and - * undercounting is how the per-field check let an oversized note through. Key order does - * not affect the total, so two encoders that order fields differently still agree. + * Measured over the note's JSON form, because that is what a snapshot carries: summing + * the text fields alone would undercount the framing every one of them is wrapped in. Key + * order does not affect the total, so two encoders that order fields differently agree. */ export function reviewNoteByteLength(note: ReviewNoteV1) { return utf8ByteLength(JSON.stringify(note)); diff --git a/src/core/review/reducer.ts b/src/core/review/reducer.ts index cef586121..7bfddbe2e 100644 --- a/src/core/review/reducer.ts +++ b/src/core/review/reducer.ts @@ -52,9 +52,9 @@ export function reduceReviewState(state: ReviewState, action: ReviewAction): Rev if (action.document === state.document) { return state; } - // Selection reconciliation is deliberately not done here: which file becomes - // selected when the current one disappears depends on the consumer's visible - // stream, so consumers dispatch the follow-up selection they want. + // Selection reconciliation is not done here: which file becomes selected when the + // current one disappears depends on the consumer's visible stream, so consumers + // dispatch the follow-up selection they want. const retired = reviewFileKeysWithRetiredContent(state.document, action.document); const expandedGaps = state.expandedGaps.filter((gap) => !retired.has(gap.fileKey)); // Loaded text is a cache of what a reader returned, not a fact of the diff: it diff --git a/src/core/review/resourceAssembly.ts b/src/core/review/resourceAssembly.ts index 14e70e3b1..1e4ac5aaf 100644 --- a/src/core/review/resourceAssembly.ts +++ b/src/core/review/resourceAssembly.ts @@ -37,6 +37,12 @@ export interface ReviewResourceChunkBytes { bytes: Uint8Array; } +/** + * What one assembler needs to verify a read it did not perform. + * + * Everything platform-shaped enters here rather than being reached for, which is why + * hashing is the injected `ReviewDigestFn` seam below. + */ export interface ReviewChunkAssemblerOptions { /** The resource being read; a chunk about anything else is a routing failure. */ resourceId: string; diff --git a/src/core/review/state.ts b/src/core/review/state.ts index 791267329..21c27510c 100644 --- a/src/core/review/state.ts +++ b/src/core/review/state.ts @@ -149,6 +149,11 @@ export interface ReviewExpandedGapState { expanded: boolean; } +/** + * Everything a review is, semantically, at one moment: the document plus what the reviewer + * has done to it. Rows, scroll offsets, widths, and themes belong to the surface drawing + * it, not here. + */ export interface ReviewState { document: ReviewDocumentV1; /** Monotonic counter advanced by every state-changing dispatch. */ diff --git a/src/core/review/types.ts b/src/core/review/types.ts index b8c84adfa..0e5804841 100644 --- a/src/core/review/types.ts +++ b/src/core/review/types.ts @@ -6,8 +6,8 @@ * transported. * * The document carries what every consumer reads. Publication addresses — generations, - * resource descriptors, digests — belong to the producer runtime and are deliberately - * absent, so nothing here implies a transport. + * resource descriptors, digests — belong to the producer runtime and are absent here, so + * nothing in this file implies a transport. */ import type { ReviewNoteSource } from "../types"; diff --git a/src/core/review/validation.ts b/src/core/review/validation.ts index 9ba63248f..cc0d36668 100644 --- a/src/core/review/validation.ts +++ b/src/core/review/validation.ts @@ -9,9 +9,9 @@ * agree — and because none of them may reach for a platform encoder or a hashing runtime * to answer these questions. * - * Hashing itself is deliberately *not* here: computing a SHA-256 needs a platform - * primitive, so it arrives as an injected `ReviewDigestFn` from whichever tier owns bytes. - * Core only names the algorithm, validates the shape, and compares two values. + * Hashing itself is *not* here: computing a SHA-256 needs a platform primitive, so it + * arrives as an injected `ReviewDigestFn` from whichever tier owns bytes. Core only names + * the algorithm, validates the shape, and compares two values. */ /** @@ -86,9 +86,9 @@ const REVIEW_SHA256_DIGEST_PATTERN = /^[0-9a-f]{64}$/; /** * Whether one value is a digest in canonical form. * - * Deliberately case-sensitive: there is one canonical spelling, and accepting both is - * what let the prototype's writer and reader disagree about whether two digests matched. - * Anything from outside is normalized on the way in rather than validated leniently. + * Case-sensitive: there is one canonical spelling, and accepting both spellings is how a + * writer and a reader come to disagree about whether two digests matched. Anything from + * outside is normalized on the way in rather than validated leniently. */ export function isReviewSha256Digest(value: unknown): value is string { return typeof value === "string" && REVIEW_SHA256_DIGEST_PATTERN.test(value); diff --git a/src/session/broker/reviewMirror.ts b/src/session/broker/reviewMirror.ts index cf8e8762c..3179f969e 100644 --- a/src/session/broker/reviewMirror.ts +++ b/src/session/broker/reviewMirror.ts @@ -6,15 +6,12 @@ * actions to it. That is the whole of what is mirrored — a position and a catalog. The * review itself stays with the session that owns it. * - * The one rule this module implements is *ordering*, and it implements it by asking - * `classifyReviewPublication` and doing what it says. The prototype's mirror had its own - * comparison — accept a lower revision never, an equal revision sometimes, a new - * generation always — which is one of the five disagreeing acceptance rules the audit - * found (`docs/browser-review-seam-audit.md`, C1). There is exactly one call to the - * classifier here and no other comparison of two publications anywhere in the daemon. + * The one rule this module implements is *ordering*, and ordering is what + * `classifyReviewPublication` says, applied verbatim — the mirror has no comparison of its + * own (`docs/browser-review-seam-audit.md`, C1). * - * A session that publishes nothing — one built before the mirror existed — is mirrored as - * nothing. That is deliberately not an error: the daemon still lists it, still brokers its + * A session that publishes nothing, such as one built before the mirror existed, is + * mirrored as nothing. That is not an error: the daemon still lists it, still brokers its * comment commands, and simply has no resources to offer on its behalf. */ import { diff --git a/src/session/reviewProtocol.ts b/src/session/reviewProtocol.ts index 2f41d9ae2..983acb81e 100644 --- a/src/session/reviewProtocol.ts +++ b/src/session/reviewProtocol.ts @@ -17,9 +17,8 @@ * of a bound that exists elsewhere. * - **Nothing is re-derived.** A caller addressing a line inside an expanded gap sends * the proof it holds (B10); the producer resolves it through `resolveReviewExpandedLine` - * and the shared anchor path. The wire never computes hunk intersections or ownership, - * which is exactly what the prototype's broker copy got wrong — its re-derivation - * omitted the fallback branch and rejected legal notes (D3). + * and the shared anchor path. The wire never computes hunk intersections or ownership + * (D3). * * The module is browser-safe by construction and gated as such: it imports from * `src/core/review/` and nothing else — no Node builtins, no broker package, no diff --git a/test/review-conformance/consumers/brokerMirror.ts b/test/review-conformance/consumers/brokerMirror.ts index 5fa763285..09b51e707 100644 --- a/test/review-conformance/consumers/brokerMirror.ts +++ b/test/review-conformance/consumers/brokerMirror.ts @@ -7,11 +7,8 @@ * the resnapshot a generation change forces, and `ignored` is everything the mirror * declined to act on. * - * That indirection is the point. The prototype's mirror compared publications itself — - * lower revisions rejected, equal ones sometimes accepted, new generations always taken — - * and the disagreement with the rest of the system only ever surfaced as a client that - * silently stopped updating (`docs/browser-review-seam-audit.md`, C1). Driving the same - * fixtures through the mirror is what proves it has no rules of its own. + * Driving the shared fixtures through the mirror's own update path is what proves it has + * no comparison rules of its own (`docs/browser-review-seam-audit.md`, C1). */ import type { ReviewPublicationAddress } from "../../../src/core/review/generationOrder"; import { ReviewMirror } from "../../../src/session/broker/reviewMirror"; diff --git a/test/review-conformance/consumers/reviewProducer.ts b/test/review-conformance/consumers/reviewProducer.ts index b66f90496..fe28a595f 100644 --- a/test/review-conformance/consumers/reviewProducer.ts +++ b/test/review-conformance/consumers/reviewProducer.ts @@ -8,8 +8,7 @@ * that addressed a different span than the manifest advertises, fails here. * * Each fixture is also self-checked at the boundary the producer actually serves: every - * canonical file it would hand out is compared against the manifest entry for it, which is - * the check three prototype implementations did three different ways (D4). + * canonical file it would hand out is compared against the manifest entry for it (D4). */ import { ReviewProducer } from "../../../src/app/review/producer"; import { assertCanonicalFileMatchesManifest } from "../../../src/core/review/canonicalFile"; diff --git a/test/review-conformance/consumers/reviewWire.ts b/test/review-conformance/consumers/reviewWire.ts index 7a7ac473e..aff99be75 100644 --- a/test/review-conformance/consumers/reviewWire.ts +++ b/test/review-conformance/consumers/reviewWire.ts @@ -8,7 +8,7 @@ * client (`docs/browser-review-seam-audit.md`, B12/B10). * * It also runs the note-size corpus, because "may this note cross a boundary" is a wire - * question the prototype answered differently from the producer (D1). + * question as much as a producer one, and both must answer it the same way (D1). */ import { reviewNoteWithinSizeLimit } from "../../../src/core/review/noteSize"; import { parseHunkReviewAction, toReviewIntent } from "../../../src/session/reviewProtocol"; diff --git a/test/review-conformance/noteSize.ts b/test/review-conformance/noteSize.ts index 2f2851050..c39628aa2 100644 --- a/test/review-conformance/noteSize.ts +++ b/test/review-conformance/noteSize.ts @@ -1,12 +1,10 @@ /** * The note-size corpus: one note, one measurement, at the boundary. * - * The prototype measured a note twice — `body` and `markup` checked separately when the - * note was admitted, the whole serialized note checked when it was published — so a note - * could pass the first check and then fail the second, taking the entire snapshot with it - * (`docs/browser-review-seam-audit.md`, D1). These fixtures are the cases that split the - * two rules apart, written from the semantics: each states the field sizes and whether the - * whole note fits. + * A note is measured whole, once, by `src/core/review/noteSize.ts` + * (`docs/browser-review-seam-audit.md`, D1). These fixtures are the cases that would split + * a whole-note measurement apart from a per-field one, written from the semantics: each + * states the field sizes and whether the whole note fits. * * Sizes are stated relative to the shared bound rather than as literals, so the corpus * still means the same thing if the bound moves. diff --git a/test/review-conformance/orderingFixtures.ts b/test/review-conformance/orderingFixtures.ts index ae8662e46..d00e4bf96 100644 --- a/test/review-conformance/orderingFixtures.ts +++ b/test/review-conformance/orderingFixtures.ts @@ -1,12 +1,9 @@ /** * The publication-ordering corpus: what is ahead, what is behind, and what needs a resnap. * - * The prototype answered this five times with three rules — one client demanded contiguous - * `+1` revisions the server never promised, another accepted equal revisions, a third - * required exact equality — and the disagreements only surfaced as a browser that silently - * stopped updating (`docs/browser-review-seam-audit.md`, C1). These fixtures pin the one - * rule from both ends: the classification itself, and the transitions a real producer - * actually emits. + * The rule these fixtures pin lives in `src/core/review/generationOrder.ts` + * (`docs/browser-review-seam-audit.md`, C1). They pin it from both ends: the + * classification itself, and the transitions a real producer actually emits. * * Verdicts are written by hand from the invariant, never captured from the classifier. */ diff --git a/test/review-conformance/types.ts b/test/review-conformance/types.ts index 08a3447f5..73563547b 100644 --- a/test/review-conformance/types.ts +++ b/test/review-conformance/types.ts @@ -203,9 +203,9 @@ export interface ReviewWireParseOutcome { /** * One consumer of the wire schema. * - * Two questions, both of which the prototype answered differently at different tiers: what - * an action means once parsed (B12/B10), and whether a note may cross a boundary at all - * (D1). A consumer joins by driving the code path it really uses. + * Two questions every tier must answer the same way: what an action means once parsed + * (B12/B10), and whether a note may cross a boundary at all (D1). A consumer joins by + * driving the code path it really uses. */ export interface ReviewWireConsumer { name: string; diff --git a/test/review-conformance/wireFixtures.ts b/test/review-conformance/wireFixtures.ts index 4464c4968..2a1fc7bf1 100644 --- a/test/review-conformance/wireFixtures.ts +++ b/test/review-conformance/wireFixtures.ts @@ -3,13 +3,11 @@ * * Every action in the vocabulary appears here with the intent it lowers to, written by * hand. That makes the corpus a statement of the round trip rather than a snapshot of the - * parser: an action that stopped lowering to the intent it derives from — the B12 failure - * mode, where a wire type drifts away from the semantics it is supposed to carry — fails - * here. + * parser: an action that stopped lowering to the intent it derives from fails here, which + * is the B12 failure mode of a wire type drifting away from the semantics it carries. * * The adversarial cases are the two the audit contributed. B10: a line inside an expanded - * gap is addressable at all, because the action carries the proof for it — the prototype's - * browser could not express one and had its clicks rejected or mis-sided. D1 is covered by + * gap is addressable at all, because the action carries the proof for it. D1 is covered by * the note-size corpus, which the wire now runs as a consumer. */ import type { ReviewWireFixture } from "./types";