Skip to content

Delta and generations

A review does not sit still. The branch moves, a work-order round lands commits, and the code under the boards changes. This page describes the two deterministic records Rennet keeps straight when that happens: Delta, the change-versus- baseline context the lens drafters read, and the successor account, the comparison of one generation with the next.

Neither is the reviewer-facing round report. A landed coding round builds that report from a separate classification of the exact worker receipt and dispatched asks. Keeping those artifacts distinct matters because they answer different questions with different inputs.

A working tree, branch, or pull request head can change. A captured patchset cannot. Recapture creates a successor instead of replacing the old input.

That is what keeps every mark attached to the material the reviewer actually saw.

One visit to a review’s boards over one patchset is a generation. Patchset identity is content identity, not visit identity: returning to the same patchset mints a new generation rather than reopening the frozen earlier visit. Inside a generation the boards are live append-only logs: re-running a lens appends, and board-native data on surviving element ids persists. When the code moves, the whole generation freezes immutable and a successor generation is minted. The frozen generation stays readable as drill-down; nothing is ever edited in place. The session URL selects a frozen generation with ?generation=<id>; without that parameter the client reads the live generation. The drill-down is therefore reloadable and directly addressable, not local switcher state.

Generation documents live in <dataDir>/generations/generations.sqlite. Every replacement increments the durable revision. Reveal and terminal writes verify the attempt identity and condition their update on that observed revision in the same SQLite write. A competing claim between the read and write leaves the newer attempt intact and suppresses the stale attempt’s progress and completion events. A round observes its predecessor before saving the successor, then freezes only that exact revision with one conditional database update. A competing write leaves the newer generation intact; the losing round records no frozen-predecessor pointer and emits no predecessor transition. Old per-generation JSON files are imported on first read and retained; the database becomes authoritative for that generation. The rounds ledger stays in its existing store.

Carry is decided by evidence, never by resemblance.

What movesRule
Board content on an element id a regenerated lens keepsCarried verbatim, with no delta stamp
Board-native data — marks, groupings, arrangement, notesCarried with the element id it sits on
An ask, thread, or highlight anchored into board proseRe-anchored only by one exact quote match in the corresponding successor lens; zero or multiple matches preserve the thread as detached and suppress its stale highlight
A code ref whose cited bytes are identical, including through a Git-proven renameResolves against the successor patchset
A code ref whose cited content changedRedrafted, and its section carries a new or reworked stamp
A code ref whose source is goneOrphaned, kept with its reason rather than reattached nearby

An occurrence is a reviewable unit inside one patchset. Its ID is scoped to that patchset. The lineage graph can describe a successor relationship as exact, one-to-one, move, split, merge, ambiguous, or terminated.

These labels describe a relationship. They do not all authorize carry. AUTO_CARRY_LINEAGES in @rennet/protocol contains only exact, and resolveAnchor() reports whether the relationship carries state.

packages/core/src/lineage-matcher.ts computes a graph from content, normalized content, path, symbol, and surrounding context. It uses global matching and emits ambiguity when competing candidates are too close. Its unit and measurement tests exercise the classifier without client repositories.

The classifier is implemented, but the live carry path does not call it. It remains a measured source of lineage data, not evidence that similar code has already inherited a human judgment.

The synthetic corpus keeps the classifier’s trade-offs executable. TP means a correct carry, FP a wrong carry, and FN a missed carry.

ClassObsFixture pairsTPFPFNFixture pass rateRecall
exact2172100100.0%100.0%
move5442166.7%80.0%
one-to-one65600100.0%100.0%
split11100100.0%100.0%
merge21200100.0%100.0%
ambiguous72700100.0%100.0%
terminated33201100.0%66.7%

Exact-only carry. The fixture pass rate is 100.0% over 21 observations from 7 independent fixture pairs. The 95% Wilson lower bound is 84.5%. Wrong carries under the live policy: 0. This must remain 0.

Move stays excluded. Enabling move auto-carry would produce 2 wrong carries on this corpus against 4 correct moves. One delete-plus-copy case reads as relocation, and one decoy keeps the old context and steals the lineage. exact carry safety rests on structure, not this synthetic-corpus pass rate. A byte-identical body at a unique body and path can only be mismatched by a SHA-256 collision. A duplicated body and path fails closed to ambiguous.

The measurement test pins this block verbatim. The corpus uses 19 synthetic patchset pairs and no client code.

The review fold in packages/core/src/index.ts compares deterministic file and span evidence in the successor patchset:

  • A byte-identical item at the same path carries.
  • A changed item reopens and leaves the active set.
  • A disappeared item moves to the orphaned set.
  • A whole-file mark on a rename reopens.
  • A span mark can carry through a Git-proven rename when its span bytes remain identical.

The distinction between the fuzzy graph and this byte comparison is deliberate and visible in the code. Similarity can help describe a successor; it does not stand in for byte evidence.

The successor account is the bridge from generation N to generation N+1. When a successor patchset has prior asks, buildSuccessorAccount() builds it deterministically. It records:

  • addressed, partially-addressed, or untouched for each ask;
  • changed paths not covered by an ask;
  • new hunks outside the asked spans;
  • handoff task attribution when present.

The account feeds the deterministic successor and Delta machinery. It does not become the artifact the reviewer reads. A landed-round report uses one separate, Council-routed classification turn over the successor patchset id, durable dispatched asks, and exact coding-turn receipt. The host copies the durable ask identity and text into a report board, then verifies every claimed evidence anchor against the measured worker diff. That verified report greets the reviewer while the boards regenerate and becomes additional round context for each lens drafter.

At path grain, every changed path is either covered by an ask or listed beyond the asks. When both patchsets are available, the account also compares hunks by their added and deleted line bytes. Context and hunk line numbers do not define a new hunk, so line-number drift alone does not appear as new work.

An unasked hunk is labeled unasked-file when no ask targeted its file, or asked-file when the file was targeted but the hunk falls outside every asked span. A truncated patch cannot support a complete hunk comparison, so that file stays at path grain.

Handoff task attribution applies to asks, not hunks. One harness turn executes the complete work order, so Rennet has no evidence that a particular task caused a particular hunk.

On re-review rounds the account also feeds buildDeltaPacket() in packages/core/src/delta/ — the folder that owns the hunk index with stable content-derived ids and the element differ, and assembles the lens drafters’ input from them. Drafters receive those deterministic facts plus the separately verified report when the round produced one.

Composition stamps each touched section of a regenerated lens board new or reworked; absence of a stamp means the section carried. Which section is which is decided by content first, then by a shared citation keyed on (path, side, start line, end line) — never on the optional symbol anchor a seat may rename or drop over the same lines, and never on the patchset the citation was minted under. A section that cites nothing falls back to its title; one that cites code does not, because two generations both writing a “Findings” section about different code is ordinary, and matching on the shared word made a new section read as a rework while hiding the old one’s removal. The marks read as unread state rather than as a changelog. Every section folds to its preview on arrival, touched or carried alike (Rai, 2026-09-04) — the reader takes the summaries first and opens what they want. A small transient accent dot per touched section is what marks it as new; it rolls up to the lens segment, clears on interaction, and is replaced wholesale by the next round’s stamps.

ConcernOwner
Lineage types and exact-only carry policypackages/protocol/src/domain.ts
Anchor resolution over a lineage graphpackages/protocol/src/delta/rsp.ts
Generation, round record, and session shapespackages/protocol/src/session/model.ts
Section delta stamps and the lens board projectionpackages/protocol/src/board/schema.ts, packages/protocol/src/board/lens-board.ts
Fuzzy occurrence classifierpackages/core/src/lineage-matcher.ts
Hunk index, element differ, and Delta-packet assemblypackages/core/src/delta/
Live carry and successor foldpackages/core/src/index.ts
Path and hunk successor accountpackages/core/src/successor-account.ts
Optional delta digest turnpackages/server/src/delta-digest-live.ts
Successor account renderingpackages/app-ui/src/components/successor-account-panel.tsx

See hand off and the exits for the rounds loop that mints each generation, and architecture contracts for the patchset invariants.