feat(web): line-numbered unified prose diff replaces the block cards (spec-by6.3.5) The block-card renderer was reviewed against a live proposal and rejected: "ADDED PARAGRAPH" outweighed the content on every row, every block carried identical chrome, and on a new file the whole page is one change, so the cards added noise and no signal. Two columns of digits say the same thing and then get out of the way, which is what a gutter is for. Selection is by line, anchoring is by block. Lines are what the cursor lands on; block hashes are what survive a reflow. The web layer maps a selected line range onto its enclosing prosediff block and stores the existing core.CommentAnchor unchanged — service/, db/ and core/ do not move and the POST wire format is untouched. The composer states which block it will anchor to before anything is typed, so the indirection is visible rather than magic. A line number is never guessed. A modified prose block goes through prosediff.WordsByLine, whose ok=false contract is honoured with a paired old/new region stating a line RANGE; a block rewritten past the similarity threshold takes that path too. An equal block that was rewrapped states an old number only for the lines the old revision really holds — equal line counts were the first rule and were not proof, which a property test over 2800 generated document/edit pairs found within seventeen cases. The markup is a table because prose wraps and a number has to stay on the first visual line of the line it names. One rail ground behind both number tracks with a single hairline against the content; the change tint starts at the sign column so the gutter never reads as part of the change; heading rows pin themselves as the section readout, replacing the per-hunk breadcrumb that only restated a heading three rows above. Folding and commenting both work with JavaScript off — the fold is a checkbox, and every block keeps a visible composer. With the script in, that per-block composer is hidden and reached by selecting lines instead, because sixteen identical "comment on this block" rows are the chrome this port removes. A composer holding typed text is never hidden by anything. Two pre-existing prosediff faults are fixed here because line numbers are what made them visible: a thematic break reported line 1 for every rule in the document, and a document whose entire content is "---" panicked in splitFrontmatter.
feat(prosediff): recover the source line each word edit sits on (spec-by6.3.5) The review UI is moving to a line-numbered unified diff, which needs to know which line a word-level change happened on. The differ does not keep that. Tokenize drops whitespace — "\n" and " " both collapse to Token.Space — and that is precisely what makes a rewrapped paragraph produce a byte-identical token stream and therefore no diff at all. The property is load-bearing, so the line is recovered here rather than retained there. It is recoverable because the script is ordered: the equal and deleted runs reproduce the old block's tokens in sequence, and the equal and inserted ones the new block's. Walking each side in step with that side's re-tokenized lines says which line every token belongs to, and a run crossing a line break is cut at the boundary. The script supplies only the operation per token; text and spacing come from re-tokenizing the source line. Taking text from the spans instead drops separators — Span.Space is false on an insertion that directly replaces a deletion, because in a combined rendering the deletion before it carried the space, and split onto one side that deletion is gone. Caught by a test: "delta CHANGED zeta" rendered as "deltaCHANGED zeta". Reports ok=false rather than guessing when a block's Lines and Text disagree about token count. A caller that cannot split falls back to rendering the block as one old/new pair labelled by line range: a wrong line number is worse than an honest range, because it invites a comment onto text that was never there. spec-by6.3.5
feat: prosediff — word-level prose diff over markdown block structure The Phase 0 de-risk gate. Segments a document into blocks with goldmark (headings, paragraphs, list items, code fences, table rows, block quotes, frontmatter), aligns the two block sequences with Myers over content hashes, and diffs word-by-word inside modified prose blocks and line-by-line inside modified code fences. Whitespace and line wrapping alone produce no diff in prose, and always do in code — that split is the whole point. Moves are detected by verbatim anchor and grown over their neighbours, so a relocated section does not explode into add+remove; a move that also bridges one edited block is recognised, a section rewritten while moving is not, and that limit is pinned by a test rather than papered over. SPIKE.md reports the verdict against twelve real revisions of docs/DESIGN.md: rewrapping the whole 1139-line document produces 1563 changed lines for git and zero changes here; 77% of real prose modifications read as small edits; 13% shred and want a two-column fallback in the web layer, which BlockChange.Similarity already gates. Verdict: the approach works, build the review UI on it.