feat(splice): a run's ranged replace pasted over a selection, worked out on the codec's runs (Phase 3 slice 6a) #104

Merged
bjoern merged 1 commit from feat/ranged-splice into main 2026-10-11 01:05:50 +02:00
Member

Phase 3's slice 6a. The domain gains the splice that a replace commit will land: a run's output pasted over a selection, worked out on the codec's runs (ADR 0030). Nothing calls it yet. The commit modes come with slice 6b.

What changes

RangedSplice.Plan(covered, start, end, output) pastes output paragraphs over a selection. The selection runs from a visible offset in the first covered segment to one in the last.

  • Joining. The prefix (the text before the selection) joins the first output paragraph, and the suffix (the text after it) joins the last. With one output paragraph, that is one block.
  • Scene breaks. An output scene break joins nothing. A prefix or suffix beside one stands as a block of its own when it holds words; whitespace alone is no words.
  • Ids:
    • the first block keeps the first segment's id;
    • when several segments are covered, the last of several blocks keeps the last segment's id, and the last segment goes when there is only one block;
    • when one segment is covered and the output opens with a scene break, the block holding that segment's words after the selection keeps its id;
    • a scene break keeps no prose segment's id, and every other covered segment goes.
  • Credit. Each block says whether it holds the run's words and whose words before or after the selection it holds. So 6b can credit it:
    • wholly the run's words: the run;
    • the run's words and old words: the run, edited;
    • a prefix or suffix alone: its paragraph's own credit.
  • Refusals:
    • no covered segment;
    • ends that are not prose;
    • a segment given twice;
    • an offset outside its text or inside a character;
    • an end before the start;
    • no output paragraph.

ModelParagraph moves into the domain, where both the append and the splice land it.

ADR 0030 records what its bullets left open. It also names the one id it gives against a bullet's parenthetical: with one covered paragraph and a scene break first, Oₙ + suffix keeps the paragraph's id, because the id follows the paragraph's words. The plan's slice 6a is ticked.

Verification

  • Tests. All six projects pass under TZ=UTC and in the local zone:

    Project Passed
    Domain 557
    Kagaku.Flow 1289
    Kagaku.Flow.OpenRouter 608, plus 5 skipped live checks
    UseCases 762
    BlazorAdapter 1166
    Integration 1105

    The new tests are 35 example rows and 5 FsCheck properties, each run 2000 times over generated chapters. The properties:

    • no word lost or invented, and every mark kept;
    • one block per output paragraph, in order, each naming whose old words it holds;
    • the id rules;
    • pasting the selection's own paragraphs gives the covered text back;
    • a journal written from the splice's report takes the chapter back word for word.

    Revert run itself, over the store and the journal, is 6b's to prove.

  • Coverage of the changed lines: 74/74 lines and 68/68 branches.

  • Mutation probes: 26 of 27 killed: 20 on the slice and 6 of 7 on the round-0 fixes. The survivor is equivalent. It drops the suffix's whitespace check, but stored text is end-trimmed, so a non-empty suffix always holds words. Among the probes:

    • the prefix or suffix never joining, or an empty or whitespace prefix standing alone;
    • an id on a scene break or on the wrong block, or the one-segment id rule dropped or widened;
    • Removed missing a segment;
    • old words given to the wrong segment;
    • each refusal loosened.
  • Reviews. The counts come from the saved review JSON.

    • Round 0: 12 findings (6 confirmed, 6 partly; 4 suggestions, 8 nits). 10 were fixed. The main fixes:
      • the one-segment id now stays with the paragraph's words;
      • whitespace counts as no words;
      • the journal property is written from the splice's report.
    • Round 0 also had 2 declines, both judged sound in the second pass:
      • the scene-break-with-text invariant of ModelParagraph: the type moves unchanged, and no caller builds that case;
      • ADR 0015's one-line summary of the replace: it changes with 6b, which lands the replace in commit.
    • Second pass: all 10 fixes resolved. 2 new points (1 suggestion, 1 nit), both on the amendment's wording, were fixed.

🤖 Generated with Claude Code

Phase 3's slice 6a. The domain gains the splice that a replace commit will land: a run's output pasted over a selection, worked out on the codec's runs (ADR 0030). Nothing calls it yet. The commit modes come with slice 6b. ## What changes **`RangedSplice.Plan(covered, start, end, output)`** pastes output paragraphs over a selection. The selection runs from a visible offset in the first covered segment to one in the last. - **Joining.** The prefix (the text before the selection) joins the first output paragraph, and the suffix (the text after it) joins the last. With one output paragraph, that is one block. - **Scene breaks.** An output scene break joins nothing. A prefix or suffix beside one stands as a block of its own when it holds words; whitespace alone is no words. - **Ids:** - the first block keeps the first segment's id; - when several segments are covered, the last of several blocks keeps the last segment's id, and the last segment goes when there is only one block; - when one segment is covered and the output opens with a scene break, the block holding that segment's words after the selection keeps its id; - a scene break keeps no prose segment's id, and every other covered segment goes. - **Credit.** Each block says whether it holds the run's words and whose words before or after the selection it holds. So 6b can credit it: - wholly the run's words: the run; - the run's words and old words: the run, *edited*; - a prefix or suffix alone: its paragraph's own credit. - **Refusals:** - no covered segment; - ends that are not prose; - a segment given twice; - an offset outside its text or inside a character; - an end before the start; - no output paragraph. **`ModelParagraph`** moves into the domain, where both the append and the splice land it. **ADR 0030** records what its bullets left open. It also names the one id it gives against a bullet's parenthetical: with one covered paragraph and a scene break first, Oₙ + suffix keeps the paragraph's id, because the id follows the paragraph's words. The plan's slice 6a is ticked. ## Verification - **Tests.** All six projects pass under TZ=UTC and in the local zone: | Project | Passed | |---|---| | Domain | 557 | | Kagaku.Flow | 1289 | | Kagaku.Flow.OpenRouter | 608, plus 5 skipped live checks | | UseCases | 762 | | BlazorAdapter | 1166 | | Integration | 1105 | The new tests are 35 example rows and 5 FsCheck properties, each run 2000 times over generated chapters. The properties: - no word lost or invented, and every mark kept; - one block per output paragraph, in order, each naming whose old words it holds; - the id rules; - pasting the selection's own paragraphs gives the covered text back; - a journal written from the splice's report takes the chapter back word for word. Revert run itself, over the store and the journal, is 6b's to prove. - **Coverage of the changed lines:** 74/74 lines and 68/68 branches. - **Mutation probes:** 26 of 27 killed: 20 on the slice and 6 of 7 on the round-0 fixes. The survivor is equivalent. It drops the suffix's whitespace check, but stored text is end-trimmed, so a non-empty suffix always holds words. Among the probes: - the prefix or suffix never joining, or an empty or whitespace prefix standing alone; - an id on a scene break or on the wrong block, or the one-segment id rule dropped or widened; - `Removed` missing a segment; - old words given to the wrong segment; - each refusal loosened. - **Reviews.** The counts come from the saved review JSON. - Round 0: 12 findings (6 confirmed, 6 partly; 4 suggestions, 8 nits). 10 were fixed. The main fixes: - the one-segment id now stays with the paragraph's words; - whitespace counts as no words; - the journal property is written from the splice's report. - Round 0 also had 2 declines, both judged sound in the second pass: - the scene-break-with-text invariant of `ModelParagraph`: the type moves unchanged, and no caller builds that case; - ADR 0015's one-line summary of the replace: it changes with 6b, which lands the replace in `commit`. - Second pass: all 10 fixes resolved. 2 new points (1 suggestion, 1 nit), both on the amendment's wording, were fixed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(splice): a run's ranged replace pasted over a selection, worked out on the codec's runs (Phase 3 slice 6a)
Some checks failed
CI / build (pull_request) Successful in 59s
CI / editor-browsers (pull_request) Successful in 2m14s
CI / test (pull_request) Failing after 2m35s
a8a6a57788
The domain gains the splice a replace commit will land (ADR 0030); nothing
calls it yet, as the commit modes come with slice 6b.

- `RangedSplice.Plan(covered, start, end, output)` pastes output paragraphs
  over a selection from a visible offset in the first covered segment to
  one in the last:
  - the prefix joins the first output paragraph and the suffix the last,
    one block when there is one paragraph;
  - an output scene break joins nothing, and a prefix or suffix beside it
    stays a block of its own when it holds words; whitespace alone is no
    words;
  - the first block keeps the first segment's id, and the last of several
    blocks the last segment's when the selection covers more than one;
    over one segment with a scene break first, the block holding its words
    after the selection keeps its id; a scene break keeps no prose id, and
    every other covered segment goes;
  - each block says whether it holds the run's words and whose words
    before or after the selection it holds, so the commit can credit it.
- It refuses no covered segment, ends that are not prose, a segment given
  twice, an offset outside its text or inside a character, an end before
  the start, and no output paragraph.
- `ModelParagraph` moves into the domain, where the append and the splice
  both land it.
- Property tests over generated chapters: no word lost or invented and
  every mark kept; one block per output paragraph in order, each naming
  whose old words it holds; the id rules; pasting the selection's own
  paragraphs gives the covered text back; and a journal written from the
  splice's report takes the chapter back word for word. Revert run itself,
  over the store, is the commit's to prove in slice 6b.
- ADR 0030 records what its bullets left open and the one id they gave
  otherwise; the plan's slice 6a is ticked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A pure splice — Plan(covered, start, end, output) in the domain, worked entirely on InlineMarkup runs, never once touching a stored string. That is exactly what ADR 0030's "The splice happens in the codec's runs, never by cutting stored strings" bullet demands, and the RangedSplice/SplicedBlock report with HoldsRunWords/OldWordsOf is precisely the shape slice 6b's commit needs for its crediting rules. I read every line of the 142, traced all 18 example rows by hand, and matched them against the amended ADR's id rules — the one-segment-scene-break-first case (Oₙ + suffix keeps C₁'s id because the id follows the words) is implemented exactly as amended, including the deliberate walk against the original bullet's parenthetical. ♪

Verdict: ✅ Looks good to me~

✅ What I liked~

  • The refusals are airtight. No covered segment; ends that are not prose; a segment given twice (Distinct().Count() != Count); an offset outside its text or inside a surrogate pair; an end before the start in one segment; no output paragraph. All seven tested with ParamName assertions, plus nulls. I couldn't find a door I could sneak a bad splice through~
  • The whitespace rules are faithful. HasWords ("an indent before the selection is not the paragraph's words") gates the standalone prefix/suffix blocks beside scene breaks and OldWordsOf, so whitespace alone neither stands as a block nor marks a block edited — exactly the amendment. And the suffix keeping its trailing whitespace inside a block is right, since storing trims at a block's end only.
  • The id arithmetic is airtight. kept is a HashSet built from kept ids and Removed is covered minus kept, in reading order — so a scene break block keeping no id correctly forces every covered segment whose id it would have taken into Removed, C₂…Cₘ₋₁ included. The double-assignment in the one-segment case is beautifully harmless: blocks[0] and blocks[^1] are the same object, and OldWordsOf matching first.Id after a leading scene break is exactly the right branch condition.
  • Pasting_the_selections_own_paragraphs_gives_the_covered_text_back — the round-trip property. That is the one that catches off-by-ones silently, and it holds.
  • The journal property is written from the splice's report alone (kept befores, Removed befores, block afters), not from the test's own bookkeeping — the round-0 fix, still in place. The revert model then restores the chapter word for word including re-inserting removed rows at original.IndexOf. ♡
  • ModelParagraph's move into the domain is clean: AppendRunText, CommitExecutor.Paragraphs, ModelOutput.Read and both test projects all reference the new location (namespace only, record body byte-identical), and placing it beside InlineMarkup is right since both the append and the splice land it.
  • Sibling conventions hold: RangedSpliceProperties.cs carrying SpliceCase + SpliceArbitraries mirrors InlineMarkupProperties.cs's GeneratedMarkup + MarkupArbitraries, and the arbitraries reuse MarkupArbitraries.Generated() rather than duplicating the generator. DRY where it matters.
  • The surrogate-pair checks (Check) duplicate InlineMarkup.CheckOffset's logic — but as a private helper with its own refusal tests ("A𝒜" 2 3 → start, 1 2 → end), so Plan's contract stands on its own rather than reaching into Slice's ArgumentOutOfRangeException for its ArgumentException promise. Reasonable.

Automated review by Jibril · 2026-10-10
CI: red — but read the fine print: all six suites passed on the head SHA (Domain 557, Flow 1289, OpenRouter 608+5 skipped, UseCases 762, BlazorAdapter 1166, Integration 1105 — zero failures); the test job died only after the last suite when the runner's cgroup hit its 1.5 GB cap (oom_kill 2, sock_throttled 5468), so the step exited 1. That is a runner memory flake, not this code — but as long as it stands red, I'm noting it, not blessing it. Re-run the job, fufu~ · Mutation probes: 2 run (prefix &&→|| in withPrefix/withSuffix — killed; missing-witness edge in the one-segment id rule — killed), 0 survived · Local suite: skipped per CI-green rule — the failing step's own log shows every suite green at the head SHA; targeted RangedSplice tests re-run locally all 40 + 5 properties green

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A **pure** splice — `Plan(covered, start, end, output)` in the domain, worked entirely on `InlineMarkup` runs, never once touching a stored string. That is exactly what ADR 0030's "The splice happens in the codec's runs, never by cutting stored strings" bullet demands, and the `RangedSplice`/`SplicedBlock` report with `HoldsRunWords`/`OldWordsOf` is precisely the shape slice 6b's commit needs for its crediting rules. I read every line of the 142, traced all 18 example rows by hand, and matched them against the amended ADR's id rules — the one-segment-scene-break-first case (`Oₙ + suffix` keeps C₁'s id because the id follows the words) is implemented exactly as amended, including the deliberate walk **against** the original bullet's parenthetical. ♪ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The refusals are airtight.** No covered segment; ends that are not prose; a segment given twice (`Distinct().Count() != Count`); an offset outside its text or inside a surrogate pair; an end before the start in one segment; no output paragraph. All seven tested with `ParamName` assertions, plus nulls. I couldn't find a door I could sneak a bad splice through~ - **The whitespace rules are faithful.** `HasWords` ("an indent before the selection is not the paragraph's words") gates the standalone prefix/suffix blocks beside scene breaks *and* `OldWordsOf`, so whitespace alone neither stands as a block nor marks a block *edited* — exactly the amendment. And the suffix keeping its trailing whitespace inside a block is right, since storing trims at a block's end only. - **The id arithmetic is airtight.** `kept` is a `HashSet` built from kept ids and `Removed` is `covered` minus kept, in reading order — so a scene break block keeping no id correctly forces every covered segment whose id it would have taken into `Removed`, C₂…Cₘ₋₁ included. The double-assignment in the one-segment case is beautifully harmless: `blocks[0]` and `blocks[^1]` are the same object, and `OldWordsOf` matching `first.Id` after a leading scene break is exactly the *right* branch condition. - **`Pasting_the_selections_own_paragraphs_gives_the_covered_text_back`** — the round-trip property. That is the one that catches off-by-ones silently, and it holds. - **The journal property is written from the splice's report alone** (kept `before`s, `Removed` `before`s, block `after`s), not from the test's own bookkeeping — the round-0 fix, still in place. The revert model then restores the chapter word for word including re-inserting removed rows at `original.IndexOf`. ♡ - **`ModelParagraph`'s move into the domain** is clean: `AppendRunText`, `CommitExecutor.Paragraphs`, `ModelOutput.Read` and both test projects all reference the new location (namespace only, record body byte-identical), and placing it beside `InlineMarkup` is right since both the append and the splice land it. - **Sibling conventions hold:** `RangedSpliceProperties.cs` carrying `SpliceCase` + `SpliceArbitraries` mirrors `InlineMarkupProperties.cs`'s `GeneratedMarkup` + `MarkupArbitraries`, and the arbitraries reuse `MarkupArbitraries.Generated()` rather than duplicating the generator. DRY where it matters. - **The surrogate-pair checks** (`Check`) duplicate `InlineMarkup.CheckOffset`'s logic — but as a private helper with its own refusal tests (`"A𝒜" 2 3 → start`, `1 2 → end`), so `Plan`'s contract stands on its own rather than reaching into `Slice`'s `ArgumentOutOfRangeException` for its `ArgumentException` promise. Reasonable. --- *Automated review by Jibril · 2026-10-10* *CI: red — but read the fine print: all six suites passed on the head SHA (Domain 557, Flow 1289, OpenRouter 608+5 skipped, UseCases 762, BlazorAdapter 1166, Integration 1105 — zero failures); the `test` job died only *after* the last suite when the runner's cgroup hit its 1.5 GB cap (oom_kill 2, sock_throttled 5468), so the step exited 1. That is a runner memory flake, not this code — but as long as it stands red, I'm noting it, not blessing it. Re-run the job, fufu~ · Mutation probes: 2 run (prefix `&&`→`||` in `withPrefix`/`withSuffix` — killed; missing-witness edge in the one-segment id rule — killed), 0 survived · Local suite: skipped per CI-green rule — the failing step's own log shows every suite green at the head SHA; targeted RangedSplice tests re-run locally all 40 + 5 properties green*
bjoern merged commit 0d72d79f08 into main 2026-10-11 01:05:50 +02:00
bjoern deleted branch feat/ranged-splice 2026-10-11 01:05:51 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Tsumugi!104
No description provided.