~bigbes/sr-ht-spec

ref: faa9163e9645bd737244305c48a2b13788dbca10 sr-ht-spec/prosediff d---------
d1621dca — Eugene Blikh 13 days ago
fix(prosediff): an equal run's separator comes from whichever side has one (spec-by6.5)

spans() read Span.Space off toks[0], and for an equal run those tokens come
from the old side. An equal run exists in both revisions at once, and the two
sides can disagree about what precedes it: a block that gains words at its head
has nothing before its first old token and the insertion before its first new
one. The script then said Space=false and a renderer joining the spans wrote
"{+in practice+}the storage layer" with the words run together — a defect the
reader would read as the author's, because nothing in the output says a
separator went missing.

Not reachable from the review page today: web/diffrows.go's sideSpans already
carries a dropped span's separator onto the next kept one, and the per-line
merge never joins two spans across that boundary. It was wrong in the data all
the same, and the next consumer of the script would have inherited it.

Passing the flag into emit rather than deriving it also drops the
write-back that reached into out[len(out)-1] to clear a substitution's
separator, which would have edited the wrong span had emit ever skipped an
empty run.
f42f81ca — Eugene Blikh 13 days ago
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.
658bae75 — Eugene Blikh 24 days ago
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
3510f9c3 — Eugene Blikh 27 days ago
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.