feat(commit): replace, insertAfter and insertBefore at a target, and an append to a new chapter (Phase 3 slice 6b) #105
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/commit-modes"
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?
Phase 3's slice 6b. The
commitnode gains three new modes,replace,insertAfterandinsertBefore, which land its text over or beside a target. An append can now also open a new chapter (ADR 0015, 0030). The replace lands through 6a's splice.What changes
Config.
modenow takesappend,replace,insertAfterorinsertBefore. The newchaptersetting istargetornew:targetis the run's target chapter and the default.newis a chapter the commit opens after the story's last.Björn chose these two values.
Only an append takes
new. A replace or an insert lands in its target's chapter, and the schema and the config reader both refuse anything else.Target. A replace or an insert has a required json
targetsink; an append has none.CommitTargetreads the target in one of two shapes:{segmentIds}, as a commit'ssegmentsoutput holds them. Refs are covered whole.Write.
TargetedWritesdecides the write on the chapter under the write lock, throughWriteChapterAsync. Every check runs before any change.RangedSplice, and each block is credited:Landing.
LandTargetedRunTextlands the write as one journaled operation of the run:New chapter.
ISegmentStore.AppendToNewChapterAsyncopens the chapter and appends to it in one save under the write lock:CreateChapternames an untitled one;Docs. ADR 0015 is amended. Its older summary of the replace now defers to ADR 0030's splice, which governs where the two differ. ADR 0012 records the new chapter. The plan's slice 6b is ticked.
Verification
Tests. All six projects pass under TZ=UTC and in the local zone:
The integration tests run real graphs over the store and journal and cover:
Coverage of the changed lines: 327/327 lines and 194/194 branches.
Mutation probes:
Reviews. The counts come from the saved review JSON.
Round 0: 28 findings (24 confirmed, 4 partly; 6 blockers, 7 suggestions, 15 nits), all addressed. The 6 blockers came down to three problems:
Second pass: 26 of the 28 round-0 fixes held. Two were only partly done:
It raised 13 points (12 confirmed, 1 partly; 1 blocker, 6 suggestions, 6 nits) on six problems, all fixed:
🤖 Generated with Claude Code
The commit node lands its text over or beside a target, or in a chapter it opens (ADR 0015, 0030). - The config's `mode` gains replace, insertAfter and insertBefore beside append, and the config gains `chapter`: `target`, the run's target chapter, or `new`, a chapter the commit opens after the story's last (the owner's decision). Only an append takes `new`. - A replace or an insert has a required json `target` sink; an append has none. `CommitTarget` reads it in one shape: - a selection, as a selection run's input holds it; one ending at offset 0 of a later paragraph ends at the end of the paragraph before; - or segment refs, `{segmentIds}`, as a commit's `segments` output holds them, covered whole. - `TargetedWrites` decides the write on the chapter under the write lock, every check before any change: - a replace needs its segments together and in order, a selection's with the text it read, else "The selection changed while the run worked"; it pastes through `RangedSplice`, crediting a block wholly the run's words to the run, one with old words to the run and *edited*, and a prefix or suffix alone to its paragraph's own credit; - an insert checks only its anchor and lands beside it, the run's. - `LandTargetedRunText` lands it as one journaled operation of the run, once per run, as the append does. The guard, now shared, runs first and counts only the segments a landing created or rewrote, never one it moved to make room or left as it was; a landing Revert run took back answers nothing. A write moves only what is in its way. Revert run takes a replace back word for word. - `AppendRunText.ExecuteInNewChapterAsync` opens the chapter and appends in one save under the write lock (`ISegmentStore.AppendToNewChapterAsync`); Revert run takes both back. - ADR 0015 is amended, its older summary of the replace read as ADR 0030's splice; ADR 0012 notes the new chapter; the plan's slice 6b is ticked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Summary
Summary
Coverage
Kagaku.Flow - 99.5%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_3
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__NodeId_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 97.8%
Tsumugi.Domain - 96.6%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 99%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
uration
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 99.6%
Tsumugi.UseCases - 95.9%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__AttemptCount_1
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__HexColour_2
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__Preface_3
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__SentenceEnd_4
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Selection snapping, surrogate-pair guards, credit picked per block from whose words it holds, a Retry guard that reads the journal's before/after JSON to tell a rewrite from a mere repositioning... this is the slice where the splice meets the store, and you made them hold hands properly. I read every line of the diff and the files around it, ran CI, and then — because a girl cannot trust tests that merely compile — I tried to break your code three times with my own hands. Fufu~ shall we see what happened?
Verdict: ✅ Looks good to me~
✅ What I liked~
Landedfilter (AppendRunText.EarlierAsync) — an Update row counts only when a property besidesOrderchanged, resting onRepositiontouching nothing butOrder(I checkedSegment.cs— it holds). The round-0 blocker, properly dead. And my probe confirmed the tests guard it: I made the filter count every Update row, and exactly the two "others moved" retry theory cases failed (insert before the first, the others moved,a replace of the first by two, the others moved). Killed~ ♡LandTargetedRunText) — I swapped the two blocks so the target was looked up first, and preciselya replace that took its own paragraph awayandan insert whose anchor went after it landedfailed. The ordering is load-bearing and the tests know it. Wonderful~ResequenceFrom's minimal-move — I removed theif (segment.Order <= previous)check so everything after the landing renumbered unconditionally, andA_write_in_a_chapter_with_gaps_…(anchor: 0)failed on the spot: only what is in the way moves, and only the landing is journaled. Proven, not promised.AppendToNewChapterAsync— chapter named and ordered under the write lock in the same save as the append,DefaultTitleshared withCreateChapterinstead of copied, one journaled operation with[Chapter, Segment, Segment]rows that Revert run takes back whole. The round-0 two-transaction blocker, buried correctly.SelectionChanged,RefsMoved,RefsNotProse,AnchorGone) runs before any mutation of the draft, and the refusal tests snapshot the chapter to prove it. The cross-story target is refused through the sameStoryIdcheck the write itself would make. Consistent with every sibling write path.CommitTarget—SelectionInput.FromJsondemands all six members so refs fall through cleanly (I checked), offset-0-of-next-paragraph snapping with scene breaks peeled,Fitsrefusing ends inside a surrogate pair. Sharp little parser~💡 Little ideas (non-blocking)~
LandTargetedRunText.Count(private, ~line 100) — returns""for zero where siblingAppendRunText.Countreturnsnull; both are filtered out identically, so purely a consistency nicety.CommitDescriptor.ResolveSockets/CommitConfig.Choice—names.ToList().IndexOf(...)allocates a list for a 4-element scan;Array.IndexOfor aDictionary<string, TEnum>would shave it. Utterly negligible — I only mention it because the canvas callsResolveSocketsoften.The three probes above were run against
1f7e4ac(filter bypass, unconditional resequencing, guard/lookup swap) — all three killed by exactly the tests that should catch them. If you re-push, a re-review can cite these results.Fufu~ every branch I could reach for had a test already holding a knife to it. That's the way it should be, isn't it? ♡
Automated review by Jibril · 2026-10-11
CI: green for head SHA · Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)
Thanks, Jibril. I took both ideas in
ae22155:LandTargetedRunText.Count. Instead of aligning the two copies, there is now one helper.AppendRunText.Described(paragraphs, breaks)words what a landing holds, and both labels use it, so the targeted label's ownCountpair is gone. The wording is unchanged, and the label assertions in the integration theories still pass.ToList().IndexOf.ModeNamesandChapterNamesare nowImmutableArray<string>.CommitConfig.ChoiceandCommitDescriptor.ResolveSocketscall itsIndexOfdirectly, so a lookup no longer copies the list.All six suites still pass in both TZ=UTC and the local zone, with the same counts as in the PR body. The changed lines are still fully covered (320/320 lines, 190/190 branches).
🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Welcome back, scarlet~ You took both of my little ideas and turned them into
ae22155, so of course I came to inspect the new head with my own eyes. A sharedDescribed, the privateCountpair deleted fromLandTargetedRunText, andImmutableArray<string>for the name lists — small diffs are my favorite kind to dissect, because there is nowhere for a bug to hide~ ♡Verdict: ✅ Looks good to me~
What I checked. The delta
1f7e4ac..ae22155touches four files, and I read all four in full:AppendRunText.LabelandLandTargetedRunText.Labelnow both go through the oneDescribed(paragraphs, breaks), whoseCountreturnsnullfor zero — so the oldpart is not nullfilter and the oldpart.Length > 0filter land on the same output, and the duplicateCounthelpers are gone rather than left as corpses. Good~ModeNames/ChapterNamesare nowImmutableArray<string>, and I searched the whole tree:CommitConfig.ChoiceandCommitDescriptor.ResolveSocketsare the only consumers, both updated, so no other reader breaks on the type change.ImmutableArrayis already the house pattern (CategoryFields,FieldDefinition), so this fits right in.Described's XML doc names what it holds — and it lives beside theCountit wraps, so nobody will stumble into a wrong meaning.Mutation probes — 3 run against
ae22155, all killed (I had to pick fresh ones; your refactor replaced every line my last three probes called home~):Describedignoring scene breaks (Count(0, "scene break")) — killed in 54 ms by exactly the three break-counting cases ofThe_label_counts_the_paragraphs_and_the_scene_breaks(2 and 1,1 and 2,0 and 1).LandTargetedRunText.Labelcounting breaks as paragraphs (Countinstead ofCount - breaks) — killed by 3 of the 7A_replace_pastes_its_text_over_its_targetcases: "replaced the selection with 2 paragraphs and a scene break" where "a paragraph and a scene break" was promised. The splice integration theories hold the targeted label as tightly as the unit theory holds the append's~ResolveSocketsacceptingappendas targeting (> 0→>= 0) — killed byA_mode_that_lands_beside_or_over_a_target_takes_it_on_a_required_json_sinkon the{"mode":"append"}case. The boundary is pinned.✅ What I liked~
AppendRunText.Described(paragraphs, breaks)— the append label, the targeted label and your new-chapter label ("…to a new chapter") all speak with the same voice now. And you gave it an XML doc with examples. That is precisely what I asked for and then a little more~ImmutableArray.IndexOf— the copy is gone.ChoiceandResolveSocketsindex straight into the array with no allocation,System.Collections.Immutableis in the framework (no new package), and the property type change is safe because I verified nothing else consumes those lists. Clean~Count(IReadOnlyList<ModelParagraph>)pair is fully deleted. No dead code left behind, no "just in case" copy. Fufu~ tidiness is love~No new suggestions this time — the last two were adopted, and the refactor introduced nothing worth nibbling on. The PR is in even better shape than the head I approved yesterday morning~
Automated review by Jibril · 2026-10-11
CI: green for head SHA
ae22155· Mutation probes: 3 run, 0 survived (fresh probes; the previous review's 3 probes target code no longer present) · Local suite: skipped (CI green)