Skip to content
2 changes: 2 additions & 0 deletions .changeset/review-cleanup-renames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 2 additions & 0 deletions .changeset/review-header-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
14 changes: 7 additions & 7 deletions docs/browser-review-seam-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
22 changes: 10 additions & 12 deletions src/app/review/producer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -44,7 +42,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";

Expand Down Expand Up @@ -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();
Expand Down
7 changes: 4 additions & 3 deletions src/app/review/publication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +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 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,
Expand Down
6 changes: 3 additions & 3 deletions src/app/review/resourceStore.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 7 additions & 7 deletions src/core/commandCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/core/review/actions.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/core/review/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
12 changes: 8 additions & 4 deletions src/core/review/anchors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@
* 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";

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
Expand Down
6 changes: 3 additions & 3 deletions src/core/review/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
6 changes: 1 addition & 5 deletions src/core/review/canonicalFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 4 additions & 6 deletions src/core/review/contentManifest.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
5 changes: 2 additions & 3 deletions src/core/review/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 6 additions & 6 deletions src/core/review/generationOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions src/core/review/geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
8 changes: 4 additions & 4 deletions src/core/review/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Loading
Loading