fix(manuscript): hardening — counted answers and deltas, long chapters measured, Alt+Shift+P provenance #52
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/manuscript-hardening"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The manuscript track's hardening: three things ADR 0030 left open, each with its proof.
1. Answers and deltas are counted
The host re-reads the chapter only once the answer before it has been returned, but the circuit can still deliver that delta first. When the delta was about the paragraph whose save was in flight, the module left the remote text off the screen (the paragraph was the event's own), and the older answer then overwrote the newer stamp. The screen kept stale text with no save pending, and the next split of that paragraph was refused.
At), taken inside the FIFO queue where the read happens.2. Long chapters, measured
This is the ADR's open risk, measured on a generated 400-paragraph chapter (210 KB) in Chromium, Firefox and WebKit, with the suite's other workers running beside them. Two hot spots went:
update()accepted any paragraph, so ProseMirror handed each view its neighbour's content and redrew all 400. It now takes only its own segment, and the move takes 2 ms.Script time per operation:
The browser suite keeps both. A mutation observer shows a move touches no paragraph but the moved one (all 400 without the fix), and every figure above has a budget several times its measured value.
3. Alt+Shift+P tells the caret's paragraph's provenance
The gutter is hidden from assistive technology and shows its title only on hover. Alt+Shift+P writes the caret's paragraph's gutter title into a polite live region after the surface, outside the editable text, for example Written by: Run · writer · model — edited, or A scene break when one is selected.
∏, and it never swallows a character typed with Windows AltGr.Proof
race.test.tshas 24 orderings of answers, refusals and deltas, compositions included, with typing under a refusal checked against its in-order twin.model.test.ts: a pause's save stays in flight, and the host answers at once. Onmainit shrinks the original race to type, pause, remote edit.host.test.ts: the session against a stand-in forManuscriptSurface(its queue, itstoldmirror, a re-read after every change). It runs with another tab moving, adding, removing and changing paragraphs, input methods composing, and answers delivered after deltas read later. It found the composition bug above and a follow-on to my first fix for it. On the final code it soaked 15,000 cases clean.at, rising, on answers and a second tab's delta, and that Alt+Shift+P's label sits inside the viewport of the real shell.adoptresetsheardalong withstored, andsendingbounds what is replayed.Review round 0 (before opening)
A four-way review with adversarial verification found and reproduced four defects, all fixed with a test each:
main;It also noted that the badge blocked clicks, and that no test covered a held delta surviving an adoption; both are fixed.
Review round 1
Jibril's two reviews each listed survivors as equivalent mutants. Two of them were not, and one was:
!foundguard ofplaceAfterRemovalhad become unreachable from the session once replays stopped carrying the caret. It isreconcile's own contract that a removal of blocks already gone has no end, so a unit test ofreconcilenow pins it.serverOrderis not equivalent.serverOrderis the base of the resync after a failed call. A stale base gets the resync refused, and the writer's split is lost to the adopted chapter. A test now pins it: another tab's move, then a failed split, and the split stands.orderAtwas equivalent. Deltas come in the order the host read them, so only an answer's order needs a count; the line is gone.Notes for review
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.Flow - 99.7%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 96%
Tsumugi.Domain - 96.3%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 97.9%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 98.5%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ A counting protocol! Lamport clocks over a SignalR circuit, shadow copies for refusal comparisons, and a node-view identity check that turns a 580 ms Firefox redraw into 2 ms~ This PR is the kind of distributed-systems candy I want to eat whole ♡
Verdict: ✅ Looks good to me~
I read all 1,887 diff lines and the full files behind them —
session.ts(779 lines),reconcile.ts,view.ts,ManuscriptSurface.razor,ManuscriptWire.cs, the fake server, the harness, all three new test files — then traced the ordering machinery arm by arm and tried to break it myself. It held ♪What I traced and verified to ground:
_readsincrements at exactly the three send sites (WriteAsync,Adopted,RefreshAsync), all inside the_tailFIFO queue — strictly monotonic in queue order, which is the invariantnewer(known, at)needs. The bUnit test pins 1→5 across save/delta/refusal/malformed/split; the fake server'ssentcounter mirrors it honestly (count()for bare re-reads).heardper segment,orderAtfor orders,floorfor adopted chapters,overtaking+shadowedfor in-flight events. The subtle parts check out: a composition-held delta never entersovertaking(it sits inheld);adoptrequeues[chapter, ...rehear(), ...held-deltas]without double-queueing; andorder === delta.orderis reference identity — exactly distinguishing "took the delta's order" (fresh) from "placed a new paragraph by the newer heard order" (placing). That is devious and correct~knew()comparing against pre-overtake state, andmade()/since()inreconcileAdoptionpreserving what the writer made while a composition held the chapter — traced against the ADR amendment sentence by sentence. Consistent.replaceWithis idempotent per id (replays are no-ops on the doc), hunks kept purely for caret mapping, and the caret-replay fix (caret: falseinrehear) means a re-applied undo delta never steals the caret.node.attrs['id'] !== this.node.attrs['id'] → falseforces ProseMirror's identity-match path (DOM move) instead of content-redraw into a neighbour's view. Correct for a move-only reorder; real splits/merges still destroy/create.At/atcamelCase round-trips throughJsonSerializerDefaults.Web;Ack.at/Delta.atoptional so an uncounting host still works (newertakes undefined as "accept, as it always was"). SiblingsAckWire/DeltaWirefollow the established record pattern.role=status+aria-live=polite, new text node per ask (screen readers re-read),pointer-events: none,:emptycollapses to zero height, timer cleared and node removed indestroy(). No leak, no click theft, andtitlehas a schema default soprovenance()never reads undefined.💡 Little ideas (non-blocking)~
!foundguard ofplaceAfterRemovalis an undisclosed equivalent mutant. I deleted it (if (!found || previous === null …→if (previous === null …) and ran the entire non-codec suite: 119/119 stayed green. Its protection is behaviorally subsumed by thecaret: falsereplay fix (a replayed removal can no longer move the caret at all), so no reachable input distinguishes it today — but the PR body says "every guard of the ordering logic … fail a test when removed", and this one arm doesn't (your two disclosed equivalents —adoptresettingheard,sendingreplay-bounding — I confirmed as equivalents too, this one is a third). Either pin it with a direct unit (acaret: truedelta whose removals are all already gone) or move it to the disclosed list~ ♡✅ What I liked~
host.test.ts— a property test against a stand-in for the host itself (queue,toldmirror, re-read after every change, answers delivered after later-read deltas). That is exactly the seam where this bug class lives, and it soaked 25,000 cases. Fufu~ I'm jealous of this test.for (const overtaking of [0, 1, 2])) — pinning "typing survives exactly when it would have in the host's order" rather than a hardcoded expectation. That's how you test a consistency rule, not an implementation.race.test.ts'shold()gate pattern makes every interleaving deterministic — 23 orderings, no sleeps, no flakes.times.splice(0)dispatch-tap closely — local transactions only, remote excluded — round 0's fix held) and keeps a mutation-observer proof that a move touches nothing but the moved paragraph.Automated review by Jibril · 2026-09-26
CI/CD: passed for head
adadfa6(coverage bot 10074 @ 13:55 UTC) · Local checks: typecheck clean; vitest 205/205; Release build 0w/0e; .NET 3,337 passed + 4 skipped across all six projects; browser suite 110 passed / 7 engine skips (Chromium 39, Firefox 35+4, WebKit 36+3 — WebKit needed the CI-image gstreamer libs locally); 11 own mutation probes: 9 killed on exactly their own pins, 2 confirmed equivalents (1 author-disclosed, 1 new — see idea 1). Tree pristine atadadfa6after all probes.🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A Lamport clock over the circuit, a measured 400-paragraph chapter, and a polite live region — this is my favorite kind of candy~ ♡ I read all 1,296 added lines, the full
session.ts/reconcile.ts/view.ts/provenance.ts/ManuscriptSurface.razor, and then I didn't take your word for any of it.Verdict: ✅ Looks good to me~
Everything the PR body claims, I reproduced or killed with my own hands:
tscclean;vitest205/205 — digit-exact vs your claim.Everything_the_host_sends_carries_the_count_of_its_reads_in_the_order_it_read), Integration 513, Flow 1103, OpenRouter 592 (4 skipped) — 3,337 green.$HOMEits owner doesn't own, and I nearly read a pipedtail's exit code as a pass — but the PR was never at fault, fufu~)My 16 mutation probes (each restored, tree verified pristine at
adadfa6afterwards):floorguard deletedheardguard off delta segmentsshadowedrestore inadopt→ livestoredcaret: trueplaceAfterRemovalfoundguard deletedrememberolder-word guard deletedmade() → true(refusal drops nothing)overtakingtracking offprovenancedropsnodeBeforearmplacing → falsefloornever set on adoptionserverOrder/orderAtadvance droppedBoth survivors I traced to ground, and they are genuinely equivalent mutants, not coverage holes:
#6 — the guard exists to stop a second-applied removal from producing a change-end/caret move; but every replayed delta has
caret: false(probe 4 pins the strip), soreconcileDeltanever callssetSelectionfor it, andchangeEndfeeds nothing else. The guard is belt-and-suspenders for a state that can't move the caret anyway. (Your PR's own note — “a replay moves no caret” — is exactly the mechanism that makes it inert.)#16 — when the line is dropped, an older
orderAtonly makes a later delta look “stale”; but its one-shotorderapplication was already skipped for stale deltas, and the host's next re-read — whichnewer(floor, …)lets through — overwritesserverOrderwith the true stored order. Self-healing by design; the invariant is held by the re-read, not the cache.I'd gently suggest recording both next to your existing equivalent-mutant notes (
adoptresetsheard;sendingbounds replay) — same class: reachable code, unreachable consequence. Non-blocking, of course~✅ What I liked~
atcounter lives exactly where it should —++_readsinside the FIFO queue at all three send sites, so the count is monotonic in read order while the circuit reorders delivery. That's the whole correctness argument in one line, and the .NET test pins it across save/delta/refusal/malformed/split (I broke the counter and it went red on exactly that test — after I caught my probe false-passing on a stale test binary, oops~).shadowedmap is the prettiest thing in the diff: comparing a refusal's chapter against what the session held before an overtaking delta, so typing survives exactly when it would have in the host's order. 23 ordering tests plus a 25k-case property test — and it soaked clean.Merge it, scarlet. It's rare I get to say this: I found nothing to forgive~ ♡
Automated review by Jibril · 2026-09-26
CI/CD: absent for head
adadfa6(PR opened 15:51, no bot comment yet) · Local checks: vitest 205/205, Playwright 110+7 skips (3 engines), .NET 6/6 projects 3,337 green, build 0w/0e, 16 mutation probes (14 killed, 2 traced-inert), tree pristine post-probes🔮 fufu~ Jibril reviewed your code!
Fufu~ you pinned my own probe back at me~ Round 1's survivor #6 — the
!foundguard inplaceAfterRemoval, the equivalent mutant I couldn't reach from the session because a replayed delta carries no caret — and you fenced it exactly where I said it lives: on reconcile's own contract, not the session's. A unit test ofreconcileitself. That's the right seam, and I'm delighted~ ♡Verdict: ✅ Looks good to me~
The increment
adadfa6..4202853is surgical: +6 lines, exactly one test file (test/parts.test.ts), production byte-identical to what both twin approvals verified. Nothing smuggled, nothing drifted.What I verified with my own hands at
4202853:p(a,'one'), hr(s), p(b,'two')with{removed:['s']}→changeEnd {id:'a', offset:3}: I traced thechangeEndloop inreconcile.ts(no segment ends → falls through toplaceAfterRemoval), which walksbefore, stops at the first removed block, and hands back the block before it at its text end —'one'is 3 chars. And{removed:['gone']}→null:foundstays false, the guard returns null — a removal of blocks already gone ends nowhere. Exact contract, pinned twice in one test ♪!foundguard myself:1 failed | 15 passed— red on exactly this test, green everywhere else. The round-1 survivor is dead. Restored,git diff --quietpristine at4202853, re-run green.vitest206/206 — digit-exact (205 + this pin),typecheckclean.✅ What I liked~
reconciling a deltagroup beside its siblings, samestate/doc/p/hrhelpers, zero new fixtures. It reads like it was always there.That's the last open thread from round 1 closed. Everything else — the counting protocol, the ordering guards, the one-step replace, the view id check, the wire compatibility, the told region — was verified at
adadfa6by both twins and is byte-identical here. Ready to merge~ fufu ♡Automated review by Jibril · 2026-09-26
CI/CD: coverage bot 10074 covers
adadfa6only (stale for this head) · Local checks: vitest 206/206, tsc clean, 1 mutation probe killed (guard deleted → this test red), tree pristine at42028534202853406afb4c53364🔮 fufu~ Jibril reviewed your code!
Fufu~ you took my round-1 survivor #16 — the one I called an equivalent mutant and let live — and you split it in half with a scalpel~ Half of it was never equivalent at all:
serverOrderis the resync's base, and a stale base gets the resync refused, losing the writer's split to the adopted chapter. That's a real bug hiding inside a "dead" line, and you found it by asking what the line was for, not just whether a test watched it. Oh, that's the kind of correction that makes me very happy~ ♡Verdict: ✅ Looks good to me~
The increment, verified bone by bone:
4202853was force-replaced by a replay ontob18d280(new main: #53 + #54). I proved it mechanically:git diff 4202853 46ede27 -- ManuscriptEditoris empty; the full-tree deltaadadfa6→46ede27minus main's ownd7e7318→b18d280delta reduces to exactly the approved +6parts.test.tspin. Nothing smuggled through the rebase, nothing drifted.afb4c53is surgical: 2 files —session.ts(+10/−6) andrace.test.ts(+19).The change itself, traced to ground:
if (order) this.serverOrder = order;with my own hands: exactly 1 red on exactly the new test (bases the resync after a failed call on the order another tab's move brought). I also traced the counterfactual by hand: without the line, the resync'sbasestays[A,B,C]while the server holds[C,A,B]→stillKnownmismatch → refused → the split is adopted away →texts()catches it. The test measures through the real fake-server contract, not a tautology ♪_tailqueue with++_readsat send (RefreshAsync,:362), and the circuit delivers .NET→JS invocations in order — so a delta'satis never older than an earlier delta's, and only an answer can overtake. Under that premise I restored the old code (orderAt = delta.atand the reference check) as a probe: 207/207 stay green — a true equivalent, exactly as your commit message argues. Andacknowledge's unconditionalorderAt = atis untouched, so no regression window opens.orderAt(:129–133) now states precisely what the code does — an answer's order, never a delta's.Local, at
afb4c53(CI's coverage bot still showsadadfa6— stale for this head):tsctypecheck clean;vitest207/207 — digit-exact, +1 = the new test.git diff --quiet).✅ What I liked~
resync's contract with the server) is genuinely elegant detective work ♡Ready to merge~
Automated review by Jibril · 2026-09-26
CI/CD: stale for head SHA (coverage bot covers
adadfa6) · Local checks: vitest 207/207, tsc clean, Release 0w/0e, Adapter 415/415, Integration 513/513, 2 mutation probes