Status: run, with a verdict.
Corpus: this repository's own docs/DESIGN.md — twelve revisions, be096fc
… db7d6c0, 336 → 1139 lines of heavily reworked English prose with genuine
reflow, written by an agent and repeatedly corrected by review. Extracted with
git show <rev>:docs/DESIGN.md. Every number and excerpt below is real output
from prosediff/cmd/prosediff; revisions are pinned to SHAs rather than HEAD
because other agents were committing to this branch while the spike ran.
Yes. Build the review UI on this. The failure mode the design feared — "a one-word edit shows as a whole-paragraph replace" — does not occur at all, and the common cases are not merely tolerable but pleasant to read. Roughly 4 in 5 real modifications read as what they are: a substitution, an added clause, a deleted sentence.
There is one real weak spot — paragraphs that were substantively rewritten shred into interleaved fragments (13% of real prose modifications) — but it is a presentation problem with a fix the data structure already supports, not a defect in the approach. Details and the recommended fix below.
Two limitations to carry into Phase 4, both honest rather than papered over:
@@ heading › path @@ banner as a stand-in and the web UI will need
something better.Rewrapping the whole document from ~80 to 58 columns, changing not one word:
| line differ | prosediff | |
|---|---|---|
db7d6c0 vs. rewrapped db7d6c0 |
1563 changed lines | 0 changes — 307 blocks, all equal |
That is the entire premise of the package, and it holds on a real 1139-line
document containing paragraphs, nested lists, block quotes, tables, YAML and
ASCII-art code fences. It is not an artefact of the test fixtures; the
equivalent synthetic check is TestWholeDocumentReflowIsSilent.
Adjacent commits are what a review actually looks like, so these matter more
than the endpoints. git --numstat on the same pair for comparison:
be096fc→c945f38 95 equal, 1 modified, 27 added, 0 removed, 0 moved | git +86 -1
c945f38→72f5894 113 equal, 8 modified, 11 added, 2 removed, 0 moved | git +76 -26
72f5894→fc49924 117 equal, 13 modified, 32 added, 2 removed, 0 moved | git +165 -46
fc49924→1e7e4b1 130 equal, 23 modified, 60 added, 9 removed, 0 moved | git +296 -89
1e7e4b1→f0dcfb4 210 equal, 2 modified, 6 added, 1 removed, 0 moved | git +35 -19
f0dcfb4→12e043e 185 equal, 18 modified, 32 added, 15 removed, 0 moved | git +125 -101
12e043e→a733623 223 equal, 5 modified, 53 added, 7 removed, 0 moved | git +242 -49
a733623→ba61af7 280 equal, 1 modified, 1 added, 0 removed, 0 moved | git +11 -1
ba61af7→7c82786 280 equal, 2 modified, 6 added, 0 removed, 0 moved | git +28 -4
7c82786→d148a66 283 equal, 4 modified, 9 added, 1 removed, 0 moved | git +42 -11
d148a66→db7d6c0 294 equal, 2 modified, 11 added, 0 removed, 0 moved | git +48 -4
The equal counts are the important column: in every pair the aligner keeps the
untouched bulk of the document silent, including across the two commits that
rewrote the largest sections (fc49924→1e7e4b1, f0dcfb4→12e043e). No
content-specific heuristics or tuning were needed to get that.
Zero moves across the whole history is a property of this corpus, not a broken detector: the document grew by insertion and rewriting rather than relocation, and where sections were reorganised they were also rewritten, which removes the verbatim anchor moves are keyed on. Move detection is therefore exercised against constructed moves below.
Yes. This is the strongest result. Appending one clause and rewrapping the
paragraph (a 7-line change to git):
@@ … › Merge model: no text merge, ever @@
~ L404 paragraph modified
**Verified constraint:** `go-git` v5.19.1 implements only `FastForwardMerge`
(`repository.go:1800`; anything else returns `ErrUnsupportedMergeStrategy`). There is no
three-way merge available in-process{+, and none is planned upstream+}.
307 blocks: 306 equal, 1 modified, 0 added, 0 removed, 0 moved (+6/-0 words)
Real commits look the same. From 7c82786 (agent-token rework):
~ L546 paragraph modified
[-Per-space (or per-project) tokens with a role — `reader` / `proposer` / `writer` —
and-]{+One agent token (no roles, no per-space scopes — see above) plus+} a **required
agent identity string**. Every commit records it in a way that survives clone:
and from the most recent pair, d148a66→db7d6c0:
~ L435→465 paragraph modified
**Deletion and rename need an explicit surface.** The write plane is whole-document `PUT`,
which gives an agent no way to express "delete this" or "move this". Two coherent answers;
**the second is the v1 [-recommendation-]{+decision+}**:
A line differ shows each of these as several deleted and several inserted lines, with no indication of which words actually moved.
Across all 69 prose modifications in the eleven pairs:
| shape of the inline diff | count | reads as |
|---|---|---|
| ≤ 2 changed runs | 27 (39%) | one clean substitution — ideal |
| 3–6 changed runs | 26 (38%) | a few separate edits in one paragraph — fine |
| 7–8 changed runs | 7 (10%) | busy, still followable |
| ≥ 9 changed runs | 9 (13%) | shredded — see below |
Similarity of paired blocks: min 0.41, p10 0.46, p25 0.63, median 0.76, p75 0.86. Ten further modifications were code fences, diffed line-by-line.
The worst case in the corpus (c945f38→72f5894, similarity 0.56, 18 changed
runs):
**[-Verified that opting out is-]{+Opting out, if we ever do, is verified+}
safe.** `api.sr.ht` federates *every* config section ending in `.sr.ht` [-—
it does not consult an-]{+with no+} allow-list, [-so our section does get
added as a federated service pointed at `-]{+pointing at `api-origin` (else
`+}origin{+`) `+} + "/query"`[-, which we will not serve-]. `updateSchema`
(`api.sr.ht/main.go`) [-handles this explicitly:-]{+logs and **skips**+}
services that are offline or [-have-]{+serve+} an invalid schema [-are
logged and **skipped**-], and `BuildSchema` runs over [-only-] the healthy
ones[-. So the effect is-]{+only — so a non-GraphQL service costs+} one
`Unable to update service [-"spec.sr.ht"-]` log line, not a broken gateway.
This is genuinely unpleasant, and it is not fixable by tuning the word
differ. The paragraph really was rewritten sentence by sentence, and
interleaving two rewritten sentences is inherently hard to read no matter how
the runs are computed. Absorbing tiny surviving fragments into the surrounding
change (the package does this — smallEqualRun) helps, but cannot rescue a
70%-rewritten paragraph.
The fix belongs in the web layer, and the data already carries the signal.
Every block with ≥ 9 changed runs has similarity ≤ 0.73; every block above 0.75
either reads cleanly or is a handful of clearly separate small edits. So
BlockChange.Similarity is a usable switch: render the inline word diff above
~0.75 and fall back to a two-column old block / new block view below it,
which is how a human wants to read a rewritten paragraph anyway. That is a
Phase 4 rendering decision, not a redesign — the field is exported for exactly
this.
Worth noting what is not happening even in the bad case: the surrounding 200+ blocks stay silent. The shredding is confined to the paragraph that was actually rewritten, so the reviewer's cost is bounded and local.
No, for moves with any verbatim content. Moving the whole "Open risks" section
(6 blocks) ahead of "Repo layout" — git reports 34 changed lines:
@@ … › GraphQL: a read schema at our own `/query` in Phase 2 @@
> L836 h2 moved here (was line 1065)
@@ … › Open risks @@
> L838 list item moved here (was line 1067)
> L841 list item moved here (was line 1070)
…
< L1065 h2 moved away (now line 836)
< L1067 list item moved away (now line 838)
…
313 blocks: 301 equal, 0 modified, 0 added, 0 removed, 6 moved (+0/-0 words)
Zero adds, zero removes. The same move with one word changed inside it also holds together — the move group bridges a single edited block, so the reviewer sees the section relocate and the one thing that actually changed in it:
> L836 h2 moved here (was line 1065)
~ L1067→838 list item modified (moved from line 1067)
**Prose diff quality (highest).** Everything downstream of the [-browser-review-]{+browser
review+} decision assumes it reads well. No fallback is designed; Phase 0 exists to find
out early rather than late.
> L841 list item moved here (was line 1070)
313 blocks: 301 equal, 1 modified, 0 added, 0 removed, 5 moved (+2/-1 words)
The honest limitation: every move must be anchored by at least one block
that is byte-identical after normalization. A section that moved and was
rewritten throughout falls through as a delete plus an insert;
TestMovedAndEditedIsNotAMove pins that behaviour so it cannot regress
silently. This is deliberate. Matching moves by similarity alone would claim
relationships between blocks that merely share boilerplate, and a confidently
wrong "moved from line 1065" costs a reviewer more than an honest add+remove.
Well, because each table row is its own block. From ba61af7, one edited
row of the reuse-inventory table with the other rows silent:
~ L574 table_row modified
| `sr-ht-core` (fork) | config, crypto, auth, database, server, **gqlgen scaffolding +
`webhooks`** | Pinned to `[-git.srht.bigb.es/~-]{+sourcecraft.dev/+}bigbes/[-core-go` via
`replace`, as in-]{+sr-ht-core v0.0.0-20260718185800-dd418a200152`, byte-identical to+}
both siblings{+. **No `replace` directive** — see the note below+}. Never `go get -u`. …
f0dcfb4→12e043e adds six rows to the decisions table; they render as six
clean + table_row entries and the surrounding rows do not move. A new table
appears as a + table_header followed by + table_row entries, which reads
correctly.
Two caveats:
|---|---| into the table
node, so changing it to |:--|--:| reports no change at all — measured,
not assumed. Acceptable (it is presentation, not content) but a real blind
spot to record.git.srht.bigb.es/~ → sourcecraft.dev/ fragment above splits mid-path
because the row is one token stream rather than a list of cells. Legible, but
cell-aware diffing would be better and is cheap to add later: the block
already records its column count.Well, and this is the second-strongest result. Each list item is its own block,
so editing one item leaves its siblings silent, adding an item is an insert
rather than a whole-list replace, and nesting depth is part of block identity —
re-indenting an item is reported (as a modification with no word changes,
flagged StructureOnly) rather than hidden.
Real evidence: 1e7e4b1→f0dcfb4 adds two list items and removes one, and the
render is exactly two + list item blocks and one - list item, each shown in
full, with the rest of their lists silent. d148a66→db7d6c0 adds a five-row
table and six paragraphs inside existing sections without disturbing one
neighbouring block.
HeadingPath is context
for the reviewer, never part of a block's hash. Including it would have made
a section rename rewrite every block underneath — a trap worth naming,
because the comment-anchoring design in DESIGN.md wants that same tuple.logout-everywhere across two lines; prosediff flagged it,
because in rendered markdown logout-\neverywhere really is
"logout- everywhere". Correct behaviour, initially mistaken for a bug.## Old name → ## New name scores 0.5, under the 0.6 floor that applies to
short blocks, so it renders as a removal plus an addition rather than a word
diff. For a two-word heading both readings are equally clear, so the floor
stays where it is rather than being tuned to a fixture
(TestShortBlockRenameFallsBackToAddRemove).Recorded so the verdict is falsifiable rather than a vibe. The spike would have failed if:
None of those happened, so the browser-review decision and Phase 4 stand.
Similarity. Inline word diff at ≥ ~0.75;
two-column old/new block below it. This removes the one genuinely bad case.ChangeMoveOut as a collapsed one-line marker and ChangeMoveIn in
full at the new position.@@ banner is a poor substitute for structural context in a browser.Block.Hash + HeadingPath + Ordinal are exactly the comment-anchor
tuple DESIGN.md specifies. Phase 5 can test anchor survival against this
same corpus rather than guessing.