fix(journal): History's Redo and Redo run land nothing on what the journal took away — each redoes whole or has nothing to redo, the store refuses a redo onto nothing, and Revert run counts an edit between two of a run's writes as a conflict #61

Merged
bjoern merged 2 commits from fix/redo-paths-refuse-nothing into main 2026-09-26 19:56:30 +02:00
Member

Summary

This covers the two follow-ups #58 left open. EfUndoStore replays an edit or a delete onto a missing entity as a no-op but still marks the row active, and two redo paths could reach that. The new tests reproduced both routes first, and they failed before the fix:

  • History's Redo onto a standing delete. A run deletes the writer's paragraph. Revert run brings it back, an edit typed on it is undone, and Redo run deletes it again. RedoRevert then answered Ok(1, 1), changed nothing, and marked "typed" active. A third route came up while fixing it. Revert run brings the paragraph back once more, and now the redo set has the run's delete below the edit. Replayed in journal order, the delete takes the paragraph and the edit lands on what it took (EF's Find returns the Deleted entry), giving Ok(2, 2) over nothing.
  • Redo run onto an undone create. A snapshot is taken, the writer writes a paragraph, and a run rewrites it. Then Revert to here on the snapshot, then Redo run: Ok(1, 1), nothing written, and the run's row marked active above the writer's undone create, which History's Redo could then no longer reach.

One landing check for all three redo paths. ReplayLanding.RedoLandsAsync walks the rows in the order the store applies them. It starts from the entities the journal has removed (IChangeJournal.RemovedAsync, #58's AnyRemovedAsync now returning which entities rather than whether), and each create and delete the redo re-applies updates that set. Ctrl+Y (RedoTyping), History's Redo (RedoRevert, and so GetStoryHistory.CanRedo, so the button and the action agree) and Redo run all ask it first.

  • Refuse as a whole, never skip rows. History's Redo brings a revert back whole, and skipped rows would wait beneath rows it re-applied, which reorders history. A run is one unit, and its conflict check already refuses it as a whole. The existing refusals are reused: "Nothing to redo." and "That run has nothing to redo."
  • Redo run checks the story as its forced discard would leave it. A discarded delete brings its entity back; a discarded edit brings nothing back. It checks before the conflicts, so a redo that cannot land never asks for confirmation, and a forced one discards nothing.

The undo direction, examined. Revert run counted conflicts only after the run's last write to an entity. A writer's polish between two of a run's writes (a Retry writing the paragraph again) stayed active in two ways:

  • If the run had created the paragraph, the polish sat over nothing, and Ctrl+Z or Revert to here then undid it over nothing.
  • Otherwise it sat over the pre-run text, and Ctrl+Z of it restored the reverted run's first write.

The window now starts at the run's first write to the entity, which is what ADR 0022's rule says. A polish discarded that way comes back only with the whole revert, through History's Redo. Redo run leaves it undone beneath the run's later write, whose words its whole-row after-state would overwrite. Redo run keeps counting from the run's last write. A polish that an earlier build's revert left active lies beneath that write, so the redo lands over it the way the run first did, with nothing to discard or confirm. UndoTyping and RevertToSnapshot reach the no-op only through rows an earlier build marked redone over nothing.

Defense in depth in the store. EfUndoStore now throws InvalidOperationException on a redo that finds its entity gone, or already Deleted earlier in the same replay. That means a bug, or another session's write between the check and the replay. Nothing is applied or marked, and the tracker is cleared so the circuit's next write carries none of it. An undo onto nothing stays a no-op that marks the row undone, so Ctrl+Z still takes back a row an earlier build marked redone over nothing, or a create whose paragraph a cascade removed.

ADR 0022 amended. Changes:

  • A new Status line, "Amended 2026-09-26 (PR #TBD, the redo paths)"; a follow-up commit fills in the number.
  • The Revert run conflict rule: it now counts from the first write, and says how a discarded in-between edit comes back.
  • The Redo bullet: Redo run's last-write window and why, the shared landing check, why each path refuses as a whole, and the store's asymmetry.
  • The Ctrl+Y paragraph now points to the shared check instead of restating a narrower rule.

Everything stays inside the journal's paths: UseCases/Journal (ReplayLanding, IChangeJournal, IUndoStore, RedoTyping, RedoRevert, RedoRun, RevertRun, GetStoryHistory), Infrastructure/Journal (EfChangeJournal, EfUndoStore), ADR 0022, HistoryAndRevertTests, and the adapter's FakeChangeJournal for the changed port member. There is no migration and no seed change.

Verification

  • Suites on the rebased head (base 2aa481f), built with -nodeReuse:false -m:2 (0 warnings, 0 errors):
    • Integration 556/556, BlazorAdapter 462/462, UseCases 243/243, Domain 486/486.
    • The journal filter (HistoryAndRevert, JournalUndoStateMigration, FieldJournal, Journal: 65 tests) passed 6 more runs in a row.
    • The Kagaku.Flow suites were not run, because nothing here touches them.
  • New tests in HistoryAndRevertTests (15 tests, 16 cases), asserting exact outcomes, texts, UndoStatus and (IsUndone, UndoState) rows:
    • Both confirmed routes and the third: History_s_Redo_lands_nothing_on_a_paragraph_whose_delete_stands_nor_on_one_its_own_delete_takes_first and Redo_run_lands_nothing_on_a_paragraph_whose_create_a_revert_to_here_took_and_History_s_Redo_brings_both_back.
    • The same for a delete: Redo_run_lands_no_delete_on_a_paragraph_whose_create_a_revert_to_here_took_and_History_s_Redo_brings_both_back.
    • Redo run's check before its confirmation, and through its discard: …says_so_before_asking_and_a_forced_one_discards_nothing, …asks_before_discarding_a_later_delete…, …counts_on_a_discarded_delete_only….
    • Revert run's window at both ends:
      • Revert_run_counts_an_edit_between_two_of_its_writes_as_a_conflict…
      • Revert_run_discards_an_edit_between_two_of_its_writes_to_the_writers_paragraph…
      • Revert_run_leaves_alone_an_edit_made_after_the_run_began_but_before_its_first_write_to_that_paragraph
    • Redo run after the in-between discard: Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice_and_leaves_the_edit_between_them_undone_beneath_the_second. Result Ok(2, 2), "By the run, again.", the polish (true, Rewound), status (false, false, null, null), and both Ctrl+Y and History's Redo return "Nothing to redo.".
    • Redo run's last-write window on rows an earlier build left: Redo_run_replays_its_later_write_over_an_edit_an_earlier_build_left_active_between_two_of_its_writes.
    • The store: The_store_refuses_a_redo_onto_nothing_and_marks_nothing (×2) and The_store_refuses_a_redo_whose_edit_finds_what_its_own_delete_took, which checks the exact message and that the next write in the same scope does not commit the half-replayed delete. Also Ctrl_Z_takes_back_an_edit_an_earlier_build_marked_redone_over_nothing and Ctrl_Z_takes_back_a_create_whose_paragraph_is_already_gone.
  • Mutation probes. Each one compiled, restored its file on every exit through a trap, and the file was touched afterwards. 26 were run (20 by the implementer, 6 in the review round), and all were killed:
Mutation Killed by
ReplayLanding: check disabled; create arm keeps the entity gone; a re-applied delete no longer marks it gone; discard loop removed; an edit in the discard counts as bringing back the route tests, …its_own_delete_takes_first, …asks_before_discarding…, …counts_on_a_discarded_delete_only…
ReplayLanding: the delete arm checked before the gone arm (review survivor) Redo_run_lands_no_delete_on_a_paragraph…
EfChangeJournal.RemovedAsync: create-undone arm dropped; delete-standing arm dropped the Redo run route; …its_own_delete_takes_first
RedoRevert skips the check; GetStoryHistory.CanRedo from the unchecked target …its_own_delete_takes_first
RedoRevert / RedoTyping target rows below the newest active one Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice…
RedoRun: check skipped; check after the discard; [] as the discard; another refusal text the Redo run tests
RedoRun: the check walks rows newest first (review survivor) Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice…
RedoRun: last-write window changed to first-write (review survivor) Redo_run_replays_its_later_write_over_an_edit_an_earlier_build_left_active…
RevertRun: Min changed to Max both in-between tests
RevertRun: the window's lower bound dropped (review survivor) Revert_run_leaves_alone_an_edit_made_…_before_its_first_write…
EfUndoStore: lenient redo of an edit or a delete; Deleted-state check dropped; ChangeTracker.Clear dropped; strict undo of an update or a create the store tests and the two Ctrl+Z tests
RedoTyping skips the check the #58 route tests and JournalUndoStateMigrationTests.A_run_s_create_undone_before_the_upgrade…

Left alone: dropping RedoRevert's target.Count == 0 short-circuit behaves exactly like the original, since it only saves a query. Forcing FakeChangeJournal.RemovedAsync to return [] survives the adapter suite. It is test infrastructure, and the inverse fails 8 adapter tests.

  • Independent review (two reviewers, both "fix-then-ready"; one reran the suites and probed 37 mutations, 5 of which survived):
    • Major: a redo delete onto nothing was untested (the delete arm checked first survived all 1241 tests). Fixed with Redo_run_lands_no_delete_on_a_paragraph….
    • Minor: Revert run's lower bound was unpinned. Fixed with Revert_run_leaves_alone_an_edit_made_….
    • Minor: the order of Redo run's landing walk was unpinned. Fixed with Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice….
    • Minor and nit: the ADR said Redo run runs "the same conflict check the other way", but its window stays at the last write, and nothing pinned that. The ADR, the amendment line and RedoRun's summary now say why, and the legacy test pins it.
    • Minor: the ADR promised Ctrl+Y brings a discarded edit back once Redo run has run, which is false for an in-between edit. The ADR and RevertRun's summary now say it comes back only with the whole revert, and the Redo run test above asserts it.
    • Minor: the store's throw is also reachable by a race (another tab's write between the check and the replay), not only by a bug. The ADR, IUndoStore and EfUndoStore now say so. Declined: wrapping the replay in WriteAsync. It would narrow only the Find→Save window, which predates this PR (a delete there already failed the save), and it cannot close the check→replay window. A race there now throws with nothing applied, leaving a consistent journal instead of the silent mark-active from before. The effects that call Ctrl+Y and History's Redo must settle on that throw; see follow-ups. Also declined: merging Redo run's discard and redo into one store call, since that is only reachable in the race and the discarded edits stay redoable.
    • Nit: the Ctrl+Y paragraph restated a narrower rule. It now points to the shared check.
    • Nit: two more parent/child cases. Added to the follow-ups below.

Follow-ups:

  • Parent/child cascade. The journal-derived check cannot see the database's ON DELETE CASCADE. None of these is reachable today, because no run creates, edits or deletes a chapter or a category yet. The shared fix is for the run conflict rules to take in a parent's rows from the same operation, and the children created under a parent the run creates or deletes. The cases:
    • Revert run of a run-created chapter or category takes the writer's paragraphs or documents in it with no journal row. Their creates stay active over nothing, and Ctrl+Y of an edit of one passes the check and now throws in the store. (Confirmed with a throwaway test, then removed.)
    • Redo run of a run's paragraph in a chapter whose create is undone fails the foreign key. (Confirmed the same way.)
    • A forced Revert run of a run's edit to a paragraph whose chapter the writer deleted since re-creates it under the missing chapter. (Found by the reviewer's reading of the code, not reproduced.)
    • Redo run of a run's chapter delete cascades over paragraphs written there after the revert. (Found by the reviewer's reading of the code, not reproduced.)
  • For the effects track. ManuscriptEffects.OnRedoAsync (Ctrl+Y) and HistoryEffects.OnRedoRevertAsync (History's Redo) should settle when the use case throws (#55's pattern), because a race can now make the store throw there.
  • The undo store and the write lock. EfUndoStore replays document and category rows without the database's write lock that ADR 0022/0028 ask of document and category writes. This predates this PR.

🤖 Generated with Claude Code

## Summary This covers the two follow-ups #58 left open. `EfUndoStore` replays an edit or a delete onto a missing entity as a no-op but still marks the row active, and two redo paths could reach that. The new tests reproduced both routes first, and they failed before the fix: - **History's Redo onto a standing delete.** A run deletes the writer's paragraph. Revert run brings it back, an edit typed on it is undone, and Redo run deletes it again. `RedoRevert` then answered `Ok(1, 1)`, changed nothing, and marked "typed" active. A third route came up while fixing it. Revert run brings the paragraph back once more, and now the redo set has the run's delete *below* the edit. Replayed in journal order, the delete takes the paragraph and the edit lands on what it took (EF's `Find` returns the `Deleted` entry), giving `Ok(2, 2)` over nothing. - **Redo run onto an undone create.** A snapshot is taken, the writer writes a paragraph, and a run rewrites it. Then Revert to here on the snapshot, then Redo run: `Ok(1, 1)`, nothing written, and the run's row marked active above the writer's undone create, which History's Redo could then no longer reach. **One landing check for all three redo paths.** `ReplayLanding.RedoLandsAsync` walks the rows in the order the store applies them. It starts from the entities the journal has removed (`IChangeJournal.RemovedAsync`, #58's `AnyRemovedAsync` now returning *which* entities rather than *whether*), and each create and delete the redo re-applies updates that set. Ctrl+Y (`RedoTyping`), History's Redo (`RedoRevert`, and so `GetStoryHistory.CanRedo`, so the button and the action agree) and Redo run all ask it first. - **Refuse as a whole, never skip rows.** History's Redo brings a revert back whole, and skipped rows would wait beneath rows it re-applied, which reorders history. A run is one unit, and its conflict check already refuses it as a whole. The existing refusals are reused: "Nothing to redo." and "That run has nothing to redo." - **Redo run checks the story as its forced discard would leave it.** A discarded delete brings its entity back; a discarded edit brings nothing back. It checks before the conflicts, so a redo that cannot land never asks for confirmation, and a forced one discards nothing. **The undo direction, examined.** Revert run counted conflicts only after the run's *last* write to an entity. A writer's polish between two of a run's writes (a Retry writing the paragraph again) stayed active in two ways: - If the run had created the paragraph, the polish sat over nothing, and Ctrl+Z or Revert to here then undid it over nothing. - Otherwise it sat over the pre-run text, and Ctrl+Z of it restored the reverted run's first write. The window now starts at the run's *first* write to the entity, which is what ADR 0022's rule says. A polish discarded that way comes back only with the whole revert, through History's Redo. Redo run leaves it undone beneath the run's later write, whose words its whole-row after-state would overwrite. Redo run keeps counting from the run's *last* write. A polish that an earlier build's revert left active lies beneath that write, so the redo lands over it the way the run first did, with nothing to discard or confirm. UndoTyping and RevertToSnapshot reach the no-op only through rows an earlier build marked redone over nothing. **Defense in depth in the store.** `EfUndoStore` now throws `InvalidOperationException` on a redo that finds its entity gone, or already `Deleted` earlier in the same replay. That means a bug, or another session's write between the check and the replay. Nothing is applied or marked, and the tracker is cleared so the circuit's next write carries none of it. An undo onto nothing stays a no-op that marks the row undone, so Ctrl+Z still takes back a row an earlier build marked redone over nothing, or a create whose paragraph a cascade removed. **ADR 0022 amended.** Changes: - A new Status line, "Amended 2026-09-26 (PR #TBD, the redo paths)"; a follow-up commit fills in the number. - The Revert run conflict rule: it now counts from the first write, and says how a discarded in-between edit comes back. - The Redo bullet: Redo run's last-write window and why, the shared landing check, why each path refuses as a whole, and the store's asymmetry. - The Ctrl+Y paragraph now points to the shared check instead of restating a narrower rule. Everything stays inside the journal's paths: `UseCases/Journal` (`ReplayLanding`, `IChangeJournal`, `IUndoStore`, `RedoTyping`, `RedoRevert`, `RedoRun`, `RevertRun`, `GetStoryHistory`), `Infrastructure/Journal` (`EfChangeJournal`, `EfUndoStore`), ADR 0022, `HistoryAndRevertTests`, and the adapter's `FakeChangeJournal` for the changed port member. There is no migration and no seed change. ## Verification - **Suites** on the rebased head (base `2aa481f`), built with `-nodeReuse:false -m:2` (0 warnings, 0 errors): - Integration 556/556, BlazorAdapter 462/462, UseCases 243/243, Domain 486/486. - The journal filter (HistoryAndRevert, JournalUndoStateMigration, FieldJournal, Journal: 65 tests) passed 6 more runs in a row. - The Kagaku.Flow suites were not run, because nothing here touches them. - **New tests** in `HistoryAndRevertTests` (15 tests, 16 cases), asserting exact outcomes, texts, `UndoStatus` and `(IsUndone, UndoState)` rows: - Both confirmed routes and the third: `History_s_Redo_lands_nothing_on_a_paragraph_whose_delete_stands_nor_on_one_its_own_delete_takes_first` and `Redo_run_lands_nothing_on_a_paragraph_whose_create_a_revert_to_here_took_and_History_s_Redo_brings_both_back`. - The same for a delete: `Redo_run_lands_no_delete_on_a_paragraph_whose_create_a_revert_to_here_took_and_History_s_Redo_brings_both_back`. - Redo run's check before its confirmation, and through its discard: `…says_so_before_asking_and_a_forced_one_discards_nothing`, `…asks_before_discarding_a_later_delete…`, `…counts_on_a_discarded_delete_only…`. - Revert run's window at both ends: - `Revert_run_counts_an_edit_between_two_of_its_writes_as_a_conflict…` - `Revert_run_discards_an_edit_between_two_of_its_writes_to_the_writers_paragraph…` - `Revert_run_leaves_alone_an_edit_made_after_the_run_began_but_before_its_first_write_to_that_paragraph` - Redo run after the in-between discard: `Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice_and_leaves_the_edit_between_them_undone_beneath_the_second`. Result `Ok(2, 2)`, "By the run, again.", the polish `(true, Rewound)`, status `(false, false, null, null)`, and both Ctrl+Y and History's Redo return "Nothing to redo.". - Redo run's last-write window on rows an earlier build left: `Redo_run_replays_its_later_write_over_an_edit_an_earlier_build_left_active_between_two_of_its_writes`. - The store: `The_store_refuses_a_redo_onto_nothing_and_marks_nothing` (×2) and `The_store_refuses_a_redo_whose_edit_finds_what_its_own_delete_took`, which checks the exact message and that the next write in the same scope does not commit the half-replayed delete. Also `Ctrl_Z_takes_back_an_edit_an_earlier_build_marked_redone_over_nothing` and `Ctrl_Z_takes_back_a_create_whose_paragraph_is_already_gone`. - **Mutation probes.** Each one compiled, restored its file on every exit through a trap, and the file was touched afterwards. 26 were run (20 by the implementer, 6 in the review round), and all were killed: | Mutation | Killed by | |---|---| | `ReplayLanding`: check disabled; create arm keeps the entity gone; a re-applied delete no longer marks it gone; discard loop removed; an edit in the discard counts as bringing back | the route tests, `…its_own_delete_takes_first`, `…asks_before_discarding…`, `…counts_on_a_discarded_delete_only…` | | `ReplayLanding`: the delete arm checked before the gone arm (review survivor) | `Redo_run_lands_no_delete_on_a_paragraph…` | | `EfChangeJournal.RemovedAsync`: create-undone arm dropped; delete-standing arm dropped | the Redo run route; `…its_own_delete_takes_first` | | `RedoRevert` skips the check; `GetStoryHistory.CanRedo` from the unchecked target | `…its_own_delete_takes_first` | | `RedoRevert` / `RedoTyping` target rows below the newest active one | `Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice…` | | `RedoRun`: check skipped; check after the discard; `[]` as the discard; another refusal text | the Redo run tests | | `RedoRun`: the check walks rows newest first (review survivor) | `Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice…` | | `RedoRun`: last-write window changed to first-write (review survivor) | `Redo_run_replays_its_later_write_over_an_edit_an_earlier_build_left_active…` | | `RevertRun`: `Min` changed to `Max` | both in-between tests | | `RevertRun`: the window's lower bound dropped (review survivor) | `Revert_run_leaves_alone_an_edit_made_…_before_its_first_write…` | | `EfUndoStore`: lenient redo of an edit or a delete; `Deleted`-state check dropped; `ChangeTracker.Clear` dropped; strict undo of an update or a create | the store tests and the two Ctrl+Z tests | | `RedoTyping` skips the check | the #58 route tests and `JournalUndoStateMigrationTests.A_run_s_create_undone_before_the_upgrade…` | Left alone: dropping `RedoRevert`'s `target.Count == 0` short-circuit behaves exactly like the original, since it only saves a query. Forcing `FakeChangeJournal.RemovedAsync` to return `[]` survives the adapter suite. It is test infrastructure, and the inverse fails 8 adapter tests. - **Independent review** (two reviewers, both "fix-then-ready"; one reran the suites and probed 37 mutations, 5 of which survived): - **Major: a redo *delete* onto nothing was untested** (the delete arm checked first survived all 1241 tests). Fixed with `Redo_run_lands_no_delete_on_a_paragraph…`. - **Minor: Revert run's lower bound was unpinned.** Fixed with `Revert_run_leaves_alone_an_edit_made_…`. - **Minor: the order of Redo run's landing walk was unpinned.** Fixed with `Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice…`. - **Minor and nit: the ADR said Redo run runs "the same conflict check the other way", but its window stays at the last write, and nothing pinned that.** The ADR, the amendment line and `RedoRun`'s summary now say why, and the legacy test pins it. - **Minor: the ADR promised Ctrl+Y brings a discarded edit back once Redo run has run, which is false for an in-between edit.** The ADR and `RevertRun`'s summary now say it comes back only with the whole revert, and the Redo run test above asserts it. - **Minor: the store's throw is also reachable by a race (another tab's write between the check and the replay), not only by a bug.** The ADR, `IUndoStore` and `EfUndoStore` now say so. Declined: wrapping the replay in `WriteAsync`. It would narrow only the Find→Save window, which predates this PR (a delete there already failed the save), and it cannot close the check→replay window. A race there now throws with nothing applied, leaving a consistent journal instead of the silent mark-active from before. The effects that call Ctrl+Y and History's Redo must settle on that throw; see follow-ups. Also declined: merging Redo run's discard and redo into one store call, since that is only reachable in the race and the discarded edits stay redoable. - **Nit: the Ctrl+Y paragraph restated a narrower rule.** It now points to the shared check. - **Nit: two more parent/child cases.** Added to the follow-ups below. **Follow-ups:** - **Parent/child cascade.** The journal-derived check cannot see the database's `ON DELETE CASCADE`. None of these is reachable today, because no run creates, edits or deletes a chapter or a category yet. The shared fix is for the run conflict rules to take in a parent's rows from the same operation, and the children created under a parent the run creates or deletes. The cases: - Revert run of a run-created chapter or category takes the writer's paragraphs or documents in it with no journal row. Their creates stay active over nothing, and Ctrl+Y of an edit of one passes the check and now throws in the store. (Confirmed with a throwaway test, then removed.) - Redo run of a run's paragraph in a chapter whose create is undone fails the foreign key. (Confirmed the same way.) - A forced Revert run of a run's edit to a paragraph whose chapter the writer deleted since re-creates it under the missing chapter. (Found by the reviewer's reading of the code, not reproduced.) - Redo run of a run's chapter delete cascades over paragraphs written there after the revert. (Found by the reviewer's reading of the code, not reproduced.) - **For the effects track.** `ManuscriptEffects.OnRedoAsync` (Ctrl+Y) and `HistoryEffects.OnRedoRevertAsync` (History's Redo) should settle when the use case throws (#55's pattern), because a race can now make the store throw there. - **The undo store and the write lock.** `EfUndoStore` replays document and category rows without the database's write lock that ADR 0022/0028 ask of document and category writes. This predates this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The follow-ups #58 left: EfUndoStore replays an edit or a delete onto a missing
entity as a no-op and still marks the row active, and two redo paths reached it.

- History's Redo: a run deletes the writer's paragraph, Revert run brings it back,
  an edit typed on it is undone, Redo run deletes it again — History's Redo
  answered Ok(1, 1), changed nothing and marked the edit active. Revert run once
  more brings the paragraph back, but the redo set is then the run's delete below
  the edit: replayed in journal order, the delete takes the paragraph and the edit
  lands on what it took (EF's Find hands back the Deleted entry) — Ok(2, 2) over
  nothing again.
- Redo run: Revert to here on a version older than the writer's paragraph, then
  Redo run of a run that rewrote it — Ok(1, 1), nothing written, the run's row
  marked active above the writer's undone create, which History's Redo then could
  no longer reach.

All three redo paths now ask one check, ReplayLanding: walked in the order the
store applies them, every row that finds its entity finds it — the journal's
removed set (IChangeJournal.RemovedAsync, #58's AnyRemovedAsync returning which
rather than whether), moved along by each create and delete the redo re-applies.
Ctrl+Y's check is now the same one. Each path refuses as a whole: History's Redo
brings a revert back whole, and rows it skipped would wait beneath rows it
re-applied; a run is one thing, as its conflict check already refuses it. History's
CanRedo reads RedoRevert.TargetAsync, so the button and the redo agree. Redo run
asks of the story as its forced discard would leave it — a discarded delete brings
its paragraph back, a discarded edit nothing — and before the conflicts, so a redo
that cannot land asks for no confirmation and a forced one discards nothing. The
existing refusals are reused: "Nothing to redo." and "That run has nothing to redo."

The undo direction, examined with throwaway tests: Revert run counted as conflicts
only rows after the run's last write to an entity. A writer's polish between two
of a run's writes (a Retry writing the paragraph again) stayed active: over nothing
when the run had created the paragraph — Ctrl+Z and Revert to here then undid it
over nothing, Ok(1, 1) — and over the pre-run text otherwise, where Ctrl+Z of it
put the reverted run's first write back. Later now means after the run's first
write to the entity, as ADR 0022's conflict rule reads; discarded, such a polish
comes back only with the whole revert, by History's Redo — Redo run leaves it
undone beneath the run's later write, whose words it would overwrite. Redo run
still counts from the run's last write: a polish an earlier build's revert left
active lies beneath that write, which the redo lands over it as the run first
did. UndoTyping and RevertToSnapshot reach the no-op otherwise only through rows
an earlier build marked redone over nothing.

Defense in depth: EfUndoStore throws on a redo that finds its entity gone, or
removed earlier in the same replay — a bug, or another session's write between
the check and the replay — applying and marking nothing and clearing the tracker
so the circuit's next write carries none of it. An undo onto nothing stays
a no-op marked undone — it has nothing to take back — so Ctrl+Z still takes back a
row an earlier build marked redone over nothing, and a create whose paragraph a
cascade took.

ADR 0022: an amendment line, the Revert run conflict rule, the Redo bullet, and
the Ctrl+Y paragraph, which now points to the landing check the three share.

Left, and reported: a revert that removes a chapter or a category a run created
takes the writer's paragraphs or documents in it through the database's cascade,
with no journal row — their creates stay active over nothing, Ctrl+Y of an edit of
one passes the journal's check and now throws in the store; and Redo run of a
run's paragraph in a chapter whose create is undone fails the foreign key. The
same class: a forced Revert run of a run's edit to a paragraph whose chapter the
writer deleted since re-creates it under the missing chapter, and Redo run of a
run's chapter delete cascades over paragraphs written there after the revert. No
run creates, edits or deletes a chapter or a category yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bjoern referenced this pull request from a commit 2026-09-26 19:34:30 +02:00
docs: the redo paths' PR is #61
All checks were successful
CI / build (pull_request) Successful in 42s
CI / editor-browsers (pull_request) Successful in 1m2s
CI / test (pull_request) Successful in 1m38s
3c272a5cc6
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

🔄 Auto-updating coverage report — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip.

Commit: 3c272a5 · Generated: 2026-09-26 17:37:55 UTC · Revision: #1

Summary

Summary
Generated on: 09/26/2026 - 17:37:55
Coverage date: 09/26/2026 - 17:37:22 - 09/26/2026 - 17:37:50
Parser: MultiReport (6x Cobertura)
Assemblies: 9
Classes: 505
Files: 454
Line coverage: 98.1% (15887 of 16190)
Covered lines: 15887
Uncovered lines: 303
Coverable lines: 16190
Total lines: 34546
Branch coverage: 94.3% (6071 of 6436)
Covered branches: 6071
Total branches: 6436
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.Flow - 99.7%
Name Line Branch
Kagaku.Flow 99.7% 99.2%
Kagaku.Flow.Budgeting.BudgetRequest 100%
Kagaku.Flow.Budgeting.ConsumerBudget 100%
Kagaku.Flow.Execution.NodeExecution 100% 100%
Kagaku.Flow.Execution.NodeOutcome 100% 100%
Kagaku.Flow.Execution.TransientFailureException 100%
Kagaku.Flow.Execution.Upstream 100%
Kagaku.Flow.Extensions.ServiceCollectionExtensions 100%
Kagaku.Flow.FlowJson 100%
Kagaku.Flow.Graphs.GraphDefinition 100%
Kagaku.Flow.Graphs.GraphDocument 100% 100%
Kagaku.Flow.Graphs.GraphEdge 100%
Kagaku.Flow.Graphs.GraphIndex 100% 100%
Kagaku.Flow.Graphs.GraphLoadError 100%
Kagaku.Flow.Graphs.GraphLoadResult 100%
Kagaku.Flow.Graphs.GraphNode 100%
Kagaku.Flow.Graphs.GraphPosition 100%
Kagaku.Flow.Graphs.GraphReader 100% 100%
Kagaku.Flow.Graphs.GraphSchema 100% 100%
Kagaku.Flow.Graphs.GraphShapeException 100%
Kagaku.Flow.Graphs.GraphWriter 100% 100%
Kagaku.Flow.Graphs.SocketRef 100%
Kagaku.Flow.Graphs.WireText 100% 100%
Kagaku.Flow.Nodes.CanvasDescriptorWriter 100% 100%
Kagaku.Flow.Nodes.ConfigProblem 100%
Kagaku.Flow.Nodes.Names 100%
Kagaku.Flow.Nodes.NodeDescriptor 100% 100%
Kagaku.Flow.Nodes.NodeRegistry 100% 100%
Kagaku.Flow.Nodes.NodeSockets 100% 100%
Kagaku.Flow.Nodes.SinkDescriptor 100%
Kagaku.Flow.Nodes.SocketTypes 100% 100%
Kagaku.Flow.Nodes.SourceDescriptor 100%
Kagaku.Flow.Nodes.TemplateField 100%
Kagaku.Flow.Runs.LedgerChange 100% 100%
Kagaku.Flow.Runs.LedgerTransitions 100% 100%
Kagaku.Flow.Runs.NodeAttempt 100%
Kagaku.Flow.Runs.NodeFacts 100%
Kagaku.Flow.Runs.NodeRun 100%
Kagaku.Flow.Runs.Run 100% 100%
Kagaku.Flow.Runs.RunEvent 100%
Kagaku.Flow.Runs.RunStatuses 100% 100%
Kagaku.Flow.Runs.RunTrigger 100% 100%
Kagaku.Flow.Scheduling.Exclusion 100%
Kagaku.Flow.Scheduling.FlowEngine 100% 100%
Kagaku.Flow.Scheduling.FlowEngineOptions 100% 100%
Kagaku.Flow.Scheduling.ReadyRow 100%
Kagaku.Flow.Scheduling.ReadySet 100% 100%
Kagaku.Flow.Scheduling.ReadySetResult 100%
Kagaku.Flow.Scheduling.RunRequest 100% 100%
Kagaku.Flow.Scheduling.RunStartResult 100%
Kagaku.Flow.Scheduling.SocketValues 100% 100%
Kagaku.Flow.Templating.InputsWalker 100% 100%
Kagaku.Flow.Templating.ParsedTemplate 100% 100%
Kagaku.Flow.Templating.SandboxedTemplateContext 100% 100%
Kagaku.Flow.Templating.TemplateError 100%
Kagaku.Flow.Templating.TemplateHost 100% 100%
Kagaku.Flow.Templating.TemplateLimits 100%
Kagaku.Flow.Templating.TemplateModel 100% 100%
Kagaku.Flow.Templating.TemplateProblem 100%
Kagaku.Flow.Templating.TemplateRenderRequest 100% 100%
Kagaku.Flow.Templating.TemplateRenderResult 100%
Kagaku.Flow.Templating.TemplateRun 100%
Kagaku.Flow.Templating.TemplateSinks 100%
Kagaku.Flow.Templating.TemplateValues 100% 100%
Kagaku.Flow.Validation.GraphValidation 100% 100%
Kagaku.Flow.Validation.GraphValidationContext 100%
Kagaku.Flow.Validation.HookFailure 100%
Kagaku.Flow.Validation.ResolvedGraph 100% 100%
Kagaku.Flow.Validation.ValidationError 100%
Kagaku.Flow.Validation.ValidationReport 100%
System.Text.RegularExpressions.Generated 97.4% 92.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
97.4% 94.4%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
97.2% 94.4%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
97.2% 94.4%
Kagaku.Flow.OpenRouter - 100%
Name Line Branch
Kagaku.Flow.OpenRouter 100% 100%
Kagaku.Flow.OpenRouter.Catalog.ModelCapabilities 100% 100%
Kagaku.Flow.OpenRouter.Gateway.LlmCompletion 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterGateway 100% 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup 100% 100%
Kagaku.Flow.OpenRouter.Gateway.SentRequestCapture 100% 100%
Kagaku.Flow.OpenRouter.Gateway.SentRequestCaptureHandler 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmAnswer 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmConfig 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmConfigRead 100%
Kagaku.Flow.OpenRouter.Llm.LlmDescriptor 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmExecutor 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmFacts 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmFailureText 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmMessage 100%
Kagaku.Flow.OpenRouter.Llm.LlmRequestBuilder 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmRequestPlan 100%
Kagaku.Flow.OpenRouter.Llm.LlmRoutingDefaults 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmSampling 100%
Kagaku.Flow.OpenRouter.Llm.LlmSchemaCheck 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmStrictSchemaRule 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmVocabulary 100% 100%
Kagaku.Flow.OpenRouter.LlmNodeServiceCollectionExtensions 100%
Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions 100% 100%
Kagaku.Flow.Testing - 100%
Name Line Branch
Kagaku.Flow.Testing 100% 100%
Kagaku.Flow.Testing.FlowEngineDriver 100% 100%
Kagaku.Flow.Testing.InMemoryRunLedger 100% 100%
Kagaku.Flow.Testing.RecordingRunEventSink 100%
Kagaku.Flow.Testing.RunLedgerContract 100% 100%
Kagaku.Flow.Testing.RunLedgerContractException 100%
Tsumugi.BlazorAdapter - 95.8%
Name Line Branch
Tsumugi.BlazorAdapter 95.8% 91.1%
Tsumugi.BlazorAdapter.BlazorAdapterAssembly 100%
Tsumugi.BlazorAdapter.Documents.CategoryCreated 100%
Tsumugi.BlazorAdapter.Documents.CategoryEditor 93.2% 88.4%
Tsumugi.BlazorAdapter.Documents.CategorySaved 100%
Tsumugi.BlazorAdapter.Documents.ChangeCategoryFieldsRequested 100%
Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryChoice 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog 100% 94.1%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentCreated 100%
Tsumugi.BlazorAdapter.Documents.DocumentEditor 95.7% 89.4%
Tsumugi.BlazorAdapter.Documents.DocumentImageRefused 100%
Tsumugi.BlazorAdapter.Documents.DocumentImageSet 100%
Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentSaved 100%
Tsumugi.BlazorAdapter.Documents.DocumentsEffects 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsList 98.5% 95%
Tsumugi.BlazorAdapter.Documents.DocumentsLoaded 100%
Tsumugi.BlazorAdapter.Documents.DocumentsPage 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsReadFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentsReducers 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsRefusal 100%
Tsumugi.BlazorAdapter.Documents.DocumentsState 100% 100%
Tsumugi.BlazorAdapter.Documents.FieldInput 100% 96.6%
Tsumugi.BlazorAdapter.Documents.FieldsChanges 100% 100%
Tsumugi.BlazorAdapter.Documents.FieldShapes 90% 87.5%
Tsumugi.BlazorAdapter.Documents.ImageUpload 100%
Tsumugi.BlazorAdapter.Documents.ImageUploadRefusal 100%
Tsumugi.BlazorAdapter.Documents.ImpactText 100% 94.2%
Tsumugi.BlazorAdapter.Documents.LoadDocuments 100%
Tsumugi.BlazorAdapter.Documents.MoveDocumentChoice 100%
Tsumugi.BlazorAdapter.Documents.MoveDocumentDialog 95.6% 71.4%
Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested 100%
Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested 100%
Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed 100%
Tsumugi.BlazorAdapter.Documents.SetDocumentImageRequested 100%
Tsumugi.BlazorAdapter.Documents.SetDocumentsFilter 100%
Tsumugi.BlazorAdapter.Documents.UpdateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.UpdateDocumentRequested 100%
Tsumugi.BlazorAdapter.Graphs.GraphCanvas 37.5% 25%
Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage 20% 25%
Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge 94.4% 83.3%
Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived 75% 100%
Tsumugi.BlazorAdapter.Runs.RunChangedBridge 97.1% 90.6%
Tsumugi.BlazorAdapter.Runs.RunChangesReceived 100%
Tsumugi.BlazorAdapter.Runs.RunTextReceived 100%
Tsumugi.BlazorAdapter.Settings.BackupCompleted 100%
Tsumugi.BlazorAdapter.Settings.BackupFailed 100%
Tsumugi.BlazorAdapter.Settings.BackupNowRequested 100%
Tsumugi.BlazorAdapter.Settings.BackupSettingsLoaded 100%
Tsumugi.BlazorAdapter.Settings.BackupsSection 100% 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsReducers 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsState 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSection 100% 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsReducers 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsState 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsLoaded 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaved 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaveFailed 100%
Tsumugi.BlazorAdapter.Settings.LoadBackupSettings 100%
Tsumugi.BlazorAdapter.Settings.LoadProviderSettings 100%
Tsumugi.BlazorAdapter.Settings.ModelCatalogLoaded 100%
Tsumugi.BlazorAdapter.Settings.ModelCatalogLoadFailed 100%
Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaved 100%
Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaveFailed 100%
Tsumugi.BlazorAdapter.Settings.ProviderKeyChecked 100%
Tsumugi.BlazorAdapter.Settings.ProviderSection 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsLoaded 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsReducers 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsState 100% 100%
Tsumugi.BlazorAdapter.Settings.SaveLlmDefaultsRequested 100%
Tsumugi.BlazorAdapter.Settings.SaveOpenRouterKeyRequested 100%
Tsumugi.BlazorAdapter.Stories.CreateStoryRequested 100%
Tsumugi.BlazorAdapter.Stories.PurgeStoryRequested 100%
Tsumugi.BlazorAdapter.Stories.RestoreStoryRequested 100%
Tsumugi.BlazorAdapter.Stories.SetShowArchived 100%
Tsumugi.BlazorAdapter.Stories.SetStoriesFilter 100%
Tsumugi.BlazorAdapter.Stories.SetStoryArchivedRequested 100%
Tsumugi.BlazorAdapter.Stories.StoriesEffects 100% 100%
Tsumugi.BlazorAdapter.Stories.StoriesLoaded 100%
Tsumugi.BlazorAdapter.Stories.StoriesPage 100% 100%
Tsumugi.BlazorAdapter.Stories.StoriesReducers 100% 100%
Tsumugi.BlazorAdapter.Stories.StoriesState 100% 100%
Tsumugi.BlazorAdapter.Stories.StoryActionFailed 100%
Tsumugi.BlazorAdapter.Stories.StoryCreated 100%
Tsumugi.BlazorAdapter.Stories.StoryCreateFailed 100%
Tsumugi.BlazorAdapter.Stories.StoryTrashPage 100% 100%
Tsumugi.BlazorAdapter.Stories.TrashStoryRequested 100%
Tsumugi.BlazorAdapter.Workspace.AckWire 100%
Tsumugi.BlazorAdapter.Workspace.AutoSaveFailed 100%
Tsumugi.BlazorAdapter.Workspace.ChangedWire 100%
Tsumugi.BlazorAdapter.Workspace.ChapterHeader 100% 86.3%
Tsumugi.BlazorAdapter.Workspace.ChapterMenu 100% 81.2%
Tsumugi.BlazorAdapter.Workspace.ChapterPage 100% 87.5%
Tsumugi.BlazorAdapter.Workspace.ChapterSaved 100%
Tsumugi.BlazorAdapter.Workspace.ChaptersPage 94.4% 83.3%
Tsumugi.BlazorAdapter.Workspace.ChapterWire 100%
Tsumugi.BlazorAdapter.Workspace.ChapterWritten 100%
Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.CreatedWire 100%
Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeltaWire 100%
Tsumugi.BlazorAdapter.Workspace.HistoryEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.HistoryLoaded 100%
Tsumugi.BlazorAdapter.Workspace.HistoryPage 100%
Tsumugi.BlazorAdapter.Workspace.HistoryPanel 100% 100%
Tsumugi.BlazorAdapter.Workspace.LoadHistory 100%
Tsumugi.BlazorAdapter.Workspace.LoadWorkspace 100%
Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.ManuscriptEvents 81.8% 74.1%
Tsumugi.BlazorAdapter.Workspace.ManuscriptSurface 89.8% 87.1%
Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.MergeEvent 100%
Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested 100%
Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.ReshapeEvent 100%
Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested 100%
Tsumugi.BlazorAdapter.Workspace.SaveScope 100% 50%
Tsumugi.BlazorAdapter.Workspace.SaveStateLadder 100% 100%
Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested 100%
Tsumugi.BlazorAdapter.Workspace.SegmentTextEvent 100%
Tsumugi.BlazorAdapter.Workspace.SegmentWire 100% 81.8%
Tsumugi.BlazorAdapter.Workspace.SelectionEndWire 100%
Tsumugi.BlazorAdapter.Workspace.SelectionGraph 100%
Tsumugi.BlazorAdapter.Workspace.SelectionRangeWire 100%
Tsumugi.BlazorAdapter.Workspace.SelectionRunRequested 100%
Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested 100%
Tsumugi.BlazorAdapter.Workspace.SplitEvent 100%
Tsumugi.BlazorAdapter.Workspace.StampWire 0%
Tsumugi.BlazorAdapter.Workspace.StoryDetails 100% 85.2%
Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage 100% 100%
Tsumugi.BlazorAdapter.Workspace.StorySaved 100%
Tsumugi.BlazorAdapter.Workspace.StoryShell 100% 100%
Tsumugi.BlazorAdapter.Workspace.UndoRequested 100%
Tsumugi.BlazorAdapter.Workspace.UndoStatusChanged 100%
Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers 100% 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceState 100%
Tsumugi.Domain - 96.3%
Name Line Branch
Tsumugi.Domain 96.3% 95%
System.Text.RegularExpressions.Generated 91.3% 88.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
97.4% 95.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
91% 91.1%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
91.8% 83.3%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
91.9% 88.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
90.9% 84.3%
Tsumugi.Domain.Documents.CategoryFields 94.5% 93.7%
Tsumugi.Domain.Documents.ChoiceOption 100%
Tsumugi.Domain.Documents.ChoiceValue 100%
Tsumugi.Domain.Documents.DateText 100%
Tsumugi.Domain.Documents.DateValue 100%
Tsumugi.Domain.Documents.Document 100% 100%
Tsumugi.Domain.Documents.DocumentCategory 100% 100%
Tsumugi.Domain.Documents.DocumentValues 100%
Tsumugi.Domain.Documents.FieldCarry 100% 100%
Tsumugi.Domain.Documents.FieldConversion 100% 100%
Tsumugi.Domain.Documents.FieldDefinition 97% 95.3%
Tsumugi.Domain.Documents.FieldMessages 100% 100%
Tsumugi.Domain.Documents.FieldNames 100%
Tsumugi.Domain.Documents.FieldsChange 100%
Tsumugi.Domain.Documents.FieldShape 95.2% 87.5%
Tsumugi.Domain.Documents.FieldsPlan 100%
Tsumugi.Domain.Documents.FieldsPlanner 99.2% 99.2%
Tsumugi.Domain.Documents.FieldValue 100%
Tsumugi.Domain.Documents.FieldValues 95.4% 97.7%
Tsumugi.Domain.Documents.ImageFiles 100% 100%
Tsumugi.Domain.Documents.ImageValue 100%
Tsumugi.Domain.Documents.MovePlan 100%
Tsumugi.Domain.Documents.NumberParse 100%
Tsumugi.Domain.Documents.NumberText 100% 97.6%
Tsumugi.Domain.Documents.NumberValue 100%
Tsumugi.Domain.Documents.StoryImage 100%
Tsumugi.Domain.Documents.TextValue 100% 100%
Tsumugi.Domain.Documents.ValueChange 100%
Tsumugi.Domain.Documents.ValueChangeKey 100%
Tsumugi.Domain.Documents.ValueImpact 100% 100%
Tsumugi.Domain.Journal.ChangeLogEntry 97.9% 83.3%
Tsumugi.Domain.Journal.StorySnapshot 95.4% 83.3%
Tsumugi.Domain.Settings.AppSetting 100%
Tsumugi.Domain.Stories.Chapter 100% 100%
Tsumugi.Domain.Stories.InlineMarkup 100% 100%
Tsumugi.Domain.Stories.MarkupRun 100%
Tsumugi.Domain.Stories.Provenance 100% 100%
Tsumugi.Domain.Stories.Segment 100% 100%
Tsumugi.Domain.Stories.Story 100% 100%
Tsumugi.Domain.Text 100% 100%
Tsumugi.Infrastructure - 98.3%
Name Line Branch
Tsumugi.Infrastructure 98.3% 86.8%
System.Text.RegularExpressions.Generated 81.4% 64.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F0D076C66E8EEC1C
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
84% 65.7%
Tsumugi.Infrastructure.Backups.BackupArtifactName 100% 100%
Tsumugi.Infrastructure.Backups.BackupOptions 100%
Tsumugi.Infrastructure.Backups.BackupSchedule 100% 83.3%
Tsumugi.Infrastructure.Backups.BackupScheduler 96.6% 87.5%
Tsumugi.Infrastructure.Backups.BackupWriter 98.5% 92.8%
Tsumugi.Infrastructure.Backups.DatabaseFingerprint 91.9% 97%
Tsumugi.Infrastructure.Backups.FileSystemBackupStore 100% 100%
Tsumugi.Infrastructure.Backups.OwnerOnlyFile 100% 100%
Tsumugi.Infrastructure.Backups.PreMigrationBackup 90% 100%
Tsumugi.Infrastructure.Backups.SqliteSnapshot 88.2% 75%
Tsumugi.Infrastructure.DependencyInjection 100% 100%
Tsumugi.Infrastructure.Journal.EfChangeJournal 100%
Tsumugi.Infrastructure.Journal.EfSnapshotStore 100%
Tsumugi.Infrastructure.Journal.EfUndoStore 98.5% 88.2%
Tsumugi.Infrastructure.Journal.ModelExtensions 100%
Tsumugi.Infrastructure.Journal.OperationContext 100% 100%
Tsumugi.Infrastructure.Journal.PropertyBag 100% 95.4%
Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus 100% 100%
Tsumugi.Infrastructure.Persistence.Configurations.AppSettingConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.ChangeLogEntryConfigurati
on
100%
Tsumugi.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.DocumentCategoryConfigura
tion
100%
Tsumugi.Infrastructure.Persistence.Configurations.DocumentConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StoryImageConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio
n
100%
Tsumugi.Infrastructure.Persistence.Converters.CanonicalJsonComparer 100%
Tsumugi.Infrastructure.Persistence.Converters.CategoryFieldsConverter 97.6% 94.4%
Tsumugi.Infrastructure.Persistence.Converters.FieldValuesConverter 96.9% 93.7%
Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter 100% 90%
Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Tsumugi.Infrastructure.Persistence.Converters.WireFormat 85.7% 73.8%
Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings 99.2%
Tsumugi.Infrastructure.Persistence.Migrations.AddJournal 98.8%
Tsumugi.Infrastructure.Persistence.Migrations.CategoryFields 100% 100%
Tsumugi.Infrastructure.Persistence.Migrations.CategoryFieldsSql 100%
Tsumugi.Infrastructure.Persistence.Migrations.DropDocumentBody 100%
Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments 100%
Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain 97.3%
Tsumugi.Infrastructure.Persistence.Migrations.JournalUndoState 100%
Tsumugi.Infrastructure.Persistence.Migrations.SegmentTextMarkup 100% 100%
Tsumugi.Infrastructure.Persistence.Migrations.SegmentTextMarkupSql 100%
Tsumugi.Infrastructure.Persistence.Migrations.StoryImages 100%
Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot 100%
Tsumugi.Infrastructure.Persistence.SqliteFunctions 80% 66.6%
Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore 98.3% 75%
Tsumugi.Infrastructure.Persistence.Stores.EfImageStore 100%
Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore 96.6% 84.6%
Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore 98.7% 90%
Tsumugi.Infrastructure.Persistence.TsumugiDbContext 97.7% 96.6%
Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory 100%
Tsumugi.Infrastructure.Provider.LlmDefaultsSource 100%
Tsumugi.Infrastructure.Provider.OpenRouterKeySource 100% 100%
Tsumugi.Infrastructure.Provider.OpenRouterModelProvider 100% 100%
Tsumugi.Infrastructure.Provider.OpenRouterOptions 100% 100%
Tsumugi.Infrastructure.Provider.ResolvedKey 100%
Tsumugi.Infrastructure.Runs.InProcessRunEventBus 100% 100%
Tsumugi.Infrastructure.Settings.EfAppSettingsStore 100% 100%
Tsumugi.Kernel - 90.9%
Name Line Branch
Tsumugi.Kernel 90.9% 75%
Tsumugi.Kernel.Err`1 100%
Tsumugi.Kernel.Ok`1 100%
Tsumugi.Kernel.Result`1 88.8% 75%
Tsumugi.Server - 100%
Name Line Branch
Tsumugi.Server 100% 92.1%
Program 100% 100%
Tsumugi.Server.Backups.BackupConfiguration 100% 100%
Tsumugi.Server.Components.App 100%
Tsumugi.Server.Components.Layout.AppHeader 100% 50%
Tsumugi.Server.Components.Layout.MainLayout 100%
Tsumugi.Server.Components.Pages.Gate 100% 100%
Tsumugi.Server.Provider.OpenRouterConfiguration 100% 100%
Tsumugi.Server.Security.AccessGate 100% 87.5%
Tsumugi.Server.Security.AccessSecret 100% 100%
Tsumugi.Server.Security.GateThrottle 100% 100%
Tsumugi.Server.VolumeStartupValidator 100% 100%
Tsumugi.UseCases - 98.8%
Name Line Branch
Tsumugi.UseCases 98.8% 96.4%
Tsumugi.UseCases.Backups.BackupArtifact 100%
Tsumugi.UseCases.Backups.BackupFailure 100%
Tsumugi.UseCases.Backups.BackupJobState 100%
Tsumugi.UseCases.Backups.BackupPolicy 100%
Tsumugi.UseCases.Backups.BackupStatusView 100% 100%
Tsumugi.UseCases.Backups.DatabaseSnapshot 100%
Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot 100%
Tsumugi.UseCases.Backups.GetBackupStatus 100%
Tsumugi.UseCases.Backups.IBackupStore 0%
Tsumugi.UseCases.Backups.RunBackupNow 100%
Tsumugi.UseCases.Chapters.ChapterDto 100%
Tsumugi.UseCases.Chapters.CreateChapter 100% 100%
Tsumugi.UseCases.Chapters.DeleteChapter 100% 100%
Tsumugi.UseCases.Chapters.GetChapterSegments 100%
Tsumugi.UseCases.Chapters.GetManuscript 100% 100%
Tsumugi.UseCases.Chapters.ManuscriptChapter 100%
Tsumugi.UseCases.Chapters.ManuscriptView 100%
Tsumugi.UseCases.Chapters.MergeChapters 100% 100%
Tsumugi.UseCases.Chapters.RenameChapter 100% 100%
Tsumugi.UseCases.Chapters.ReorderChapters 100% 100%
Tsumugi.UseCases.Chapters.SetChapterSummary 100% 75%
Tsumugi.UseCases.Context.DocumentRendering 100% 100%
Tsumugi.UseCases.Context.StoryDocumentsRule 100% 100%
Tsumugi.UseCases.Context.StoryReads 100% 100%
Tsumugi.UseCases.Context.StoryScope 100% 100%
Tsumugi.UseCases.Context.StoryTemplateModel 100%
Tsumugi.UseCases.DependencyInjection 100%
Tsumugi.UseCases.Diagnostics.SeedDevData 98.3% 68.7%
Tsumugi.UseCases.Diagnostics.SeedPictures 100%
Tsumugi.UseCases.Documents.ChangeCategoryFields 100% 83.3%
Tsumugi.UseCases.Documents.CreateCategory 100% 100%
Tsumugi.UseCases.Documents.CreateDocument 100% 100%
Tsumugi.UseCases.Documents.DefaultFields 100% 100%
Tsumugi.UseCases.Documents.DeleteCategory 100% 94.4%
Tsumugi.UseCases.Documents.DeleteDocument 100% 100%
Tsumugi.UseCases.Documents.DocumentCategoryDto 100%
Tsumugi.UseCases.Documents.DocumentCategoryProfile 100%
Tsumugi.UseCases.Documents.DocumentDto 100%
Tsumugi.UseCases.Documents.DocumentPatch 100%
Tsumugi.UseCases.Documents.DocumentProfile 100%
Tsumugi.UseCases.Documents.DocumentsCategory 100%
Tsumugi.UseCases.Documents.DocumentsView 100%
Tsumugi.UseCases.Documents.DocumentWrites 100% 100%
Tsumugi.UseCases.Documents.FieldsChangeLabel 95.4% 88.2%
Tsumugi.UseCases.Documents.GetDocuments 100% 100%
Tsumugi.UseCases.Documents.GetStoryImage 100% 100%
Tsumugi.UseCases.Documents.ImpactPreview 100% 100%
Tsumugi.UseCases.Documents.LossMessages 92.3% 87.5%
Tsumugi.UseCases.Documents.MoveDocuments 100% 100%
Tsumugi.UseCases.Documents.ReorderCategories 100% 100%
Tsumugi.UseCases.Documents.SetDocumentImage 100% 93.7%
Tsumugi.UseCases.Documents.StoryImageFile 100%
Tsumugi.UseCases.Documents.UpdateCategory 100% 100%
Tsumugi.UseCases.Documents.UpdateDocument 100% 81.2%
Tsumugi.UseCases.Documents.WriteRefusedException 100%
Tsumugi.UseCases.Journal.GetStoryHistory 100% 100%
Tsumugi.UseCases.Journal.GetUndoStatus 100% 100%
Tsumugi.UseCases.Journal.HistoryOperation 66.6%
Tsumugi.UseCases.Journal.HistorySnapshot 87.5%
Tsumugi.UseCases.Journal.OperationGrouping 100% 100%
Tsumugi.UseCases.Journal.RedoRevert 100% 100%
Tsumugi.UseCases.Journal.RedoRun 90.9% 87.5%
Tsumugi.UseCases.Journal.RedoTyping 100% 100%
Tsumugi.UseCases.Journal.ReplayLanding 100% 100%
Tsumugi.UseCases.Journal.RevertConflict 100%
Tsumugi.UseCases.Journal.RevertOutcome 100%
Tsumugi.UseCases.Journal.RevertRun 96.5% 77.7%
Tsumugi.UseCases.Journal.RevertToSnapshot 92.3% 83.3%
Tsumugi.UseCases.Journal.StoryHistory 100%
Tsumugi.UseCases.Journal.TakeSnapshot 100% 87.5%
Tsumugi.UseCases.Journal.UndoStatus 100%
Tsumugi.UseCases.Journal.UndoTyping 100% 100%
Tsumugi.UseCases.Journal.UserOperation 100%
Tsumugi.UseCases.Notifications.DomainChanged 100%
Tsumugi.UseCases.Ordering 100% 100%
Tsumugi.UseCases.Runs.RunEvent 100%
Tsumugi.UseCases.Segments.ChangedText 100%
Tsumugi.UseCases.Segments.ChapterDraft 100%
Tsumugi.UseCases.Segments.ChapterWriting 100%
Tsumugi.UseCases.Segments.DeleteSegments 100% 100%
Tsumugi.UseCases.Segments.EditSegment 100% 100%
Tsumugi.UseCases.Segments.InsertSegment 100% 100%
Tsumugi.UseCases.Segments.ManuscriptAnswer 100%
Tsumugi.UseCases.Segments.ManuscriptWrites 99.1% 97.6%
Tsumugi.UseCases.Segments.MergeSegments 100%
Tsumugi.UseCases.Segments.MergeWrite 100%
Tsumugi.UseCases.Segments.ModelOutput 98.2% 96.6%
Tsumugi.UseCases.Segments.ModelParagraph 100%
Tsumugi.UseCases.Segments.MoveSegments 100% 100%
Tsumugi.UseCases.Segments.NewBlock 100%
Tsumugi.UseCases.Segments.ReadSelection 100%
Tsumugi.UseCases.Segments.ReorderSegments 100% 100%
Tsumugi.UseCases.Segments.ReshapeSegments 100% 100%
Tsumugi.UseCases.Segments.ReshapeWrite 100%
Tsumugi.UseCases.Segments.SegmentDto 100%
Tsumugi.UseCases.Segments.SelectedSegment 100%
Tsumugi.UseCases.Segments.SelectionEnd 100%
Tsumugi.UseCases.Segments.SelectionInput 100% 100%
Tsumugi.UseCases.Segments.SplitSegment 100%
Tsumugi.UseCases.Segments.SplitWrite 100%
Tsumugi.UseCases.Segments.StampedId 100%
Tsumugi.UseCases.Segments.TextWrite 100%
Tsumugi.UseCases.Settings.CatalogModel 100% 100%
Tsumugi.UseCases.Settings.CheckOpenRouterKey 100%
Tsumugi.UseCases.Settings.GetLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.GetModelCatalog 100% 100%
Tsumugi.UseCases.Settings.GetProviderSettings 100%
Tsumugi.UseCases.Settings.LlmDefaults 100%
Tsumugi.UseCases.Settings.ProviderKeyInfo 100%
Tsumugi.UseCases.Settings.ProviderKeyState 100%
Tsumugi.UseCases.Settings.ProviderSettingsView 100% 100%
Tsumugi.UseCases.Settings.ReasoningEfforts 100% 100%
Tsumugi.UseCases.Settings.RemoveOpenRouterKey 100%
Tsumugi.UseCases.Settings.RoutingDefaults 100%
Tsumugi.UseCases.Settings.RoutingSettings 100% 100%
Tsumugi.UseCases.Settings.SaveLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.SaveOpenRouterKey 100% 100%
Tsumugi.UseCases.Stories.ChapterWriteOutcome 50%
Tsumugi.UseCases.Stories.CreateStory 100% 100%
Tsumugi.UseCases.Stories.GetStory 100% 100%
Tsumugi.UseCases.Stories.ListStories 100% 75%
Tsumugi.UseCases.Stories.PurgeStory 100% 100%
Tsumugi.UseCases.Stories.RestoreStory 100% 100%
Tsumugi.UseCases.Stories.SetActiveGraph 100% 100%
Tsumugi.UseCases.Stories.SetStoryArchived 100% 100%
Tsumugi.UseCases.Stories.StarterCategories 100%
Tsumugi.UseCases.Stories.StoryDetail 100%
Tsumugi.UseCases.Stories.StoryListItem 81.8%
Tsumugi.UseCases.Stories.StoryProfile 100%
Tsumugi.UseCases.Stories.TrashStory 100% 100%
Tsumugi.UseCases.Stories.UpdateStory 100% 100%
<!-- coverage-comment --> > 🔄 **Auto-updating coverage report** — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip. > > **Commit:** `3c272a5` · **Generated:** 2026-09-26 17:37:55 UTC · **Revision:** #1 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/26/2026 - 17:37:55 | | Coverage date: | 09/26/2026 - 17:37:22 - 09/26/2026 - 17:37:50 | | Parser: | MultiReport (6x Cobertura) | | Assemblies: | 9 | | Classes: | 505 | | Files: | 454 | | **Line coverage:** | 98.1% (15887 of 16190) | | Covered lines: | 15887 | | Uncovered lines: | 303 | | Coverable lines: | 16190 | | Total lines: | 34546 | | **Branch coverage:** | 94.3% (6071 of 6436) | | Covered branches: | 6071 | | Total branches: | 6436 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.Flow - 99.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow**|**99.7%**|**99.2%**| |Kagaku.Flow.Budgeting.BudgetRequest|100%|| |Kagaku.Flow.Budgeting.ConsumerBudget|100%|| |Kagaku.Flow.Execution.NodeExecution|100%|100%| |Kagaku.Flow.Execution.NodeOutcome|100%|100%| |Kagaku.Flow.Execution.TransientFailureException|100%|| |Kagaku.Flow.Execution.Upstream|100%|| |Kagaku.Flow.Extensions.ServiceCollectionExtensions|100%|| |Kagaku.Flow.FlowJson|100%|| |Kagaku.Flow.Graphs.GraphDefinition|100%|| |Kagaku.Flow.Graphs.GraphDocument|100%|100%| |Kagaku.Flow.Graphs.GraphEdge|100%|| |Kagaku.Flow.Graphs.GraphIndex|100%|100%| |Kagaku.Flow.Graphs.GraphLoadError|100%|| |Kagaku.Flow.Graphs.GraphLoadResult|100%|| |Kagaku.Flow.Graphs.GraphNode|100%|| |Kagaku.Flow.Graphs.GraphPosition|100%|| |Kagaku.Flow.Graphs.GraphReader|100%|100%| |Kagaku.Flow.Graphs.GraphSchema|100%|100%| |Kagaku.Flow.Graphs.GraphShapeException|100%|| |Kagaku.Flow.Graphs.GraphWriter|100%|100%| |Kagaku.Flow.Graphs.SocketRef|100%|| |Kagaku.Flow.Graphs.WireText|100%|100%| |Kagaku.Flow.Nodes.CanvasDescriptorWriter|100%|100%| |Kagaku.Flow.Nodes.ConfigProblem|100%|| |Kagaku.Flow.Nodes.Names|100%|| |Kagaku.Flow.Nodes.NodeDescriptor|100%|100%| |Kagaku.Flow.Nodes.NodeRegistry|100%|100%| |Kagaku.Flow.Nodes.NodeSockets|100%|100%| |Kagaku.Flow.Nodes.SinkDescriptor|100%|| |Kagaku.Flow.Nodes.SocketTypes|100%|100%| |Kagaku.Flow.Nodes.SourceDescriptor|100%|| |Kagaku.Flow.Nodes.TemplateField|100%|| |Kagaku.Flow.Runs.LedgerChange|100%|100%| |Kagaku.Flow.Runs.LedgerTransitions|100%|100%| |Kagaku.Flow.Runs.NodeAttempt|100%|| |Kagaku.Flow.Runs.NodeFacts|100%|| |Kagaku.Flow.Runs.NodeRun|100%|| |Kagaku.Flow.Runs.Run|100%|100%| |Kagaku.Flow.Runs.RunEvent|100%|| |Kagaku.Flow.Runs.RunStatuses|100%|100%| |Kagaku.Flow.Runs.RunTrigger|100%|100%| |Kagaku.Flow.Scheduling.Exclusion|100%|| |Kagaku.Flow.Scheduling.FlowEngine|100%|100%| |Kagaku.Flow.Scheduling.FlowEngineOptions|100%|100%| |Kagaku.Flow.Scheduling.ReadyRow|100%|| |Kagaku.Flow.Scheduling.ReadySet|100%|100%| |Kagaku.Flow.Scheduling.ReadySetResult|100%|| |Kagaku.Flow.Scheduling.RunRequest|100%|100%| |Kagaku.Flow.Scheduling.RunStartResult|100%|| |Kagaku.Flow.Scheduling.SocketValues|100%|100%| |Kagaku.Flow.Templating.InputsWalker|100%|100%| |Kagaku.Flow.Templating.ParsedTemplate|100%|100%| |Kagaku.Flow.Templating.SandboxedTemplateContext|100%|100%| |Kagaku.Flow.Templating.TemplateError|100%|| |Kagaku.Flow.Templating.TemplateHost|100%|100%| |Kagaku.Flow.Templating.TemplateLimits|100%|| |Kagaku.Flow.Templating.TemplateModel|100%|100%| |Kagaku.Flow.Templating.TemplateProblem|100%|| |Kagaku.Flow.Templating.TemplateRenderRequest|100%|100%| |Kagaku.Flow.Templating.TemplateRenderResult|100%|| |Kagaku.Flow.Templating.TemplateRun|100%|| |Kagaku.Flow.Templating.TemplateSinks|100%|| |Kagaku.Flow.Templating.TemplateValues|100%|100%| |Kagaku.Flow.Validation.GraphValidation|100%|100%| |Kagaku.Flow.Validation.GraphValidationContext|100%|| |Kagaku.Flow.Validation.HookFailure|100%|| |Kagaku.Flow.Validation.ResolvedGraph|100%|100%| |Kagaku.Flow.Validation.ValidationError|100%|| |Kagaku.Flow.Validation.ValidationReport|100%|| |System.Text.RegularExpressions.Generated|97.4%|92.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF<br/>E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2|97.4%|94.4%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF<br/>E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1|97.2%|94.4%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF<br/>E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0|97.2%|94.4%| </details> <details><summary>Kagaku.Flow.OpenRouter - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow.OpenRouter**|**100%**|**100%**| |Kagaku.Flow.OpenRouter.Catalog.ModelCapabilities|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.LlmCompletion|100%|| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterGateway|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions|100%|| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.SentRequestCapture|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.SentRequestCaptureHandler|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmAnswer|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmConfig|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmConfigRead|100%|| |Kagaku.Flow.OpenRouter.Llm.LlmDescriptor|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmExecutor|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmFacts|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmFailureText|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmMessage|100%|| |Kagaku.Flow.OpenRouter.Llm.LlmRequestBuilder|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmRequestPlan|100%|| |Kagaku.Flow.OpenRouter.Llm.LlmRoutingDefaults|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmSampling|100%|| |Kagaku.Flow.OpenRouter.Llm.LlmSchemaCheck|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmStrictSchemaRule|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmVocabulary|100%|100%| |Kagaku.Flow.OpenRouter.LlmNodeServiceCollectionExtensions|100%|| |Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions|100%|100%| </details> <details><summary>Kagaku.Flow.Testing - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow.Testing**|**100%**|**100%**| |Kagaku.Flow.Testing.FlowEngineDriver|100%|100%| |Kagaku.Flow.Testing.InMemoryRunLedger|100%|100%| |Kagaku.Flow.Testing.RecordingRunEventSink|100%|| |Kagaku.Flow.Testing.RunLedgerContract|100%|100%| |Kagaku.Flow.Testing.RunLedgerContractException|100%|| </details> <details><summary>Tsumugi.BlazorAdapter - 95.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**95.8%**|**91.1%**| |Tsumugi.BlazorAdapter.BlazorAdapterAssembly|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryCreated|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryEditor|93.2%|88.4%| |Tsumugi.BlazorAdapter.Documents.CategorySaved|100%|| |Tsumugi.BlazorAdapter.Documents.ChangeCategoryFieldsRequested|100%|| |Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryChoice|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog|100%|94.1%| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentCreated|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentEditor|95.7%|89.4%| |Tsumugi.BlazorAdapter.Documents.DocumentImageRefused|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentImageSet|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentSaved|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsEffects|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsList|98.5%|95%| |Tsumugi.BlazorAdapter.Documents.DocumentsLoaded|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsPage|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsReadFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsReducers|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsRefusal|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsState|100%|100%| |Tsumugi.BlazorAdapter.Documents.FieldInput|100%|96.6%| |Tsumugi.BlazorAdapter.Documents.FieldsChanges|100%|100%| |Tsumugi.BlazorAdapter.Documents.FieldShapes|90%|87.5%| |Tsumugi.BlazorAdapter.Documents.ImageUpload|100%|| |Tsumugi.BlazorAdapter.Documents.ImageUploadRefusal|100%|| |Tsumugi.BlazorAdapter.Documents.ImpactText|100%|94.2%| |Tsumugi.BlazorAdapter.Documents.LoadDocuments|100%|| |Tsumugi.BlazorAdapter.Documents.MoveDocumentChoice|100%|| |Tsumugi.BlazorAdapter.Documents.MoveDocumentDialog|95.6%|71.4%| |Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested|100%|| |Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested|100%|| |Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed|100%|| |Tsumugi.BlazorAdapter.Documents.SetDocumentImageRequested|100%|| |Tsumugi.BlazorAdapter.Documents.SetDocumentsFilter|100%|| |Tsumugi.BlazorAdapter.Documents.UpdateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.UpdateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Graphs.GraphCanvas|37.5%|25%| |Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage|20%|25%| |Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge|94.4%|83.3%| |Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived|75%|100%| |Tsumugi.BlazorAdapter.Runs.RunChangedBridge|97.1%|90.6%| |Tsumugi.BlazorAdapter.Runs.RunChangesReceived|100%|| |Tsumugi.BlazorAdapter.Runs.RunTextReceived|100%|| |Tsumugi.BlazorAdapter.Settings.BackupCompleted|100%|| |Tsumugi.BlazorAdapter.Settings.BackupFailed|100%|| |Tsumugi.BlazorAdapter.Settings.BackupNowRequested|100%|| |Tsumugi.BlazorAdapter.Settings.BackupSettingsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.BackupsSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsReducers|100%|| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsState|100%|| |Tsumugi.BlazorAdapter.Settings.DefaultsSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsReducers|100%|| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsState|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaved|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaveFailed|100%|| |Tsumugi.BlazorAdapter.Settings.LoadBackupSettings|100%|| |Tsumugi.BlazorAdapter.Settings.LoadProviderSettings|100%|| |Tsumugi.BlazorAdapter.Settings.ModelCatalogLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.ModelCatalogLoadFailed|100%|| |Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaved|100%|| |Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaveFailed|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderKeyChecked|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsReducers|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsState|100%|100%| |Tsumugi.BlazorAdapter.Settings.SaveLlmDefaultsRequested|100%|| |Tsumugi.BlazorAdapter.Settings.SaveOpenRouterKeyRequested|100%|| |Tsumugi.BlazorAdapter.Stories.CreateStoryRequested|100%|| |Tsumugi.BlazorAdapter.Stories.PurgeStoryRequested|100%|| |Tsumugi.BlazorAdapter.Stories.RestoreStoryRequested|100%|| |Tsumugi.BlazorAdapter.Stories.SetShowArchived|100%|| |Tsumugi.BlazorAdapter.Stories.SetStoriesFilter|100%|| |Tsumugi.BlazorAdapter.Stories.SetStoryArchivedRequested|100%|| |Tsumugi.BlazorAdapter.Stories.StoriesEffects|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoriesLoaded|100%|| |Tsumugi.BlazorAdapter.Stories.StoriesPage|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoriesReducers|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoriesState|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoryActionFailed|100%|| |Tsumugi.BlazorAdapter.Stories.StoryCreated|100%|| |Tsumugi.BlazorAdapter.Stories.StoryCreateFailed|100%|| |Tsumugi.BlazorAdapter.Stories.StoryTrashPage|100%|100%| |Tsumugi.BlazorAdapter.Stories.TrashStoryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.AckWire|100%|| |Tsumugi.BlazorAdapter.Workspace.AutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.ChangedWire|100%|| |Tsumugi.BlazorAdapter.Workspace.ChapterHeader|100%|86.3%| |Tsumugi.BlazorAdapter.Workspace.ChapterMenu|100%|81.2%| |Tsumugi.BlazorAdapter.Workspace.ChapterPage|100%|87.5%| |Tsumugi.BlazorAdapter.Workspace.ChapterSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.ChaptersPage|94.4%|83.3%| |Tsumugi.BlazorAdapter.Workspace.ChapterWire|100%|| |Tsumugi.BlazorAdapter.Workspace.ChapterWritten|100%|| |Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.CreatedWire|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeltaWire|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.HistoryLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryPage|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryPanel|100%|100%| |Tsumugi.BlazorAdapter.Workspace.LoadHistory|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadWorkspace|100%|| |Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.ManuscriptEvents|81.8%|74.1%| |Tsumugi.BlazorAdapter.Workspace.ManuscriptSurface|89.8%|87.1%| |Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MergeEvent|100%|| |Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.ReshapeEvent|100%|| |Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SaveScope|100%|50%| |Tsumugi.BlazorAdapter.Workspace.SaveStateLadder|100%|100%| |Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SegmentTextEvent|100%|| |Tsumugi.BlazorAdapter.Workspace.SegmentWire|100%|81.8%| |Tsumugi.BlazorAdapter.Workspace.SelectionEndWire|100%|| |Tsumugi.BlazorAdapter.Workspace.SelectionGraph|100%|| |Tsumugi.BlazorAdapter.Workspace.SelectionRangeWire|100%|| |Tsumugi.BlazorAdapter.Workspace.SelectionRunRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SplitEvent|100%|| |Tsumugi.BlazorAdapter.Workspace.StampWire|0%|| |Tsumugi.BlazorAdapter.Workspace.StoryDetails|100%|85.2%| |Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage|100%|100%| |Tsumugi.BlazorAdapter.Workspace.StorySaved|100%|| |Tsumugi.BlazorAdapter.Workspace.StoryShell|100%|100%| |Tsumugi.BlazorAdapter.Workspace.UndoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.UndoStatusChanged|100%|| |Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers|100%|100%| |Tsumugi.BlazorAdapter.Workspace.WorkspaceState|100%|| </details> <details><summary>Tsumugi.Domain - 96.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Domain**|**96.3%**|**95%**| |System.Text.RegularExpressions.Generated|91.3%|88.6%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2|97.4%|95.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0|91%|91.1%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1|91.8%|83.3%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4|91.9%|88.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3|90.9%|84.3%| |Tsumugi.Domain.Documents.CategoryFields|94.5%|93.7%| |Tsumugi.Domain.Documents.ChoiceOption|100%|| |Tsumugi.Domain.Documents.ChoiceValue|100%|| |Tsumugi.Domain.Documents.DateText|100%|| |Tsumugi.Domain.Documents.DateValue|100%|| |Tsumugi.Domain.Documents.Document|100%|100%| |Tsumugi.Domain.Documents.DocumentCategory|100%|100%| |Tsumugi.Domain.Documents.DocumentValues|100%|| |Tsumugi.Domain.Documents.FieldCarry|100%|100%| |Tsumugi.Domain.Documents.FieldConversion|100%|100%| |Tsumugi.Domain.Documents.FieldDefinition|97%|95.3%| |Tsumugi.Domain.Documents.FieldMessages|100%|100%| |Tsumugi.Domain.Documents.FieldNames|100%|| |Tsumugi.Domain.Documents.FieldsChange|100%|| |Tsumugi.Domain.Documents.FieldShape|95.2%|87.5%| |Tsumugi.Domain.Documents.FieldsPlan|100%|| |Tsumugi.Domain.Documents.FieldsPlanner|99.2%|99.2%| |Tsumugi.Domain.Documents.FieldValue|100%|| |Tsumugi.Domain.Documents.FieldValues|95.4%|97.7%| |Tsumugi.Domain.Documents.ImageFiles|100%|100%| |Tsumugi.Domain.Documents.ImageValue|100%|| |Tsumugi.Domain.Documents.MovePlan|100%|| |Tsumugi.Domain.Documents.NumberParse|100%|| |Tsumugi.Domain.Documents.NumberText|100%|97.6%| |Tsumugi.Domain.Documents.NumberValue|100%|| |Tsumugi.Domain.Documents.StoryImage|100%|| |Tsumugi.Domain.Documents.TextValue|100%|100%| |Tsumugi.Domain.Documents.ValueChange|100%|| |Tsumugi.Domain.Documents.ValueChangeKey|100%|| |Tsumugi.Domain.Documents.ValueImpact|100%|100%| |Tsumugi.Domain.Journal.ChangeLogEntry|97.9%|83.3%| |Tsumugi.Domain.Journal.StorySnapshot|95.4%|83.3%| |Tsumugi.Domain.Settings.AppSetting|100%|| |Tsumugi.Domain.Stories.Chapter|100%|100%| |Tsumugi.Domain.Stories.InlineMarkup|100%|100%| |Tsumugi.Domain.Stories.MarkupRun|100%|| |Tsumugi.Domain.Stories.Provenance|100%|100%| |Tsumugi.Domain.Stories.Segment|100%|100%| |Tsumugi.Domain.Stories.Story|100%|100%| |Tsumugi.Domain.Text|100%|100%| </details> <details><summary>Tsumugi.Infrastructure - 98.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Infrastructure**|**98.3%**|**86.8%**| |System.Text.RegularExpressions.Generated|81.4%|64.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F0D076C66E8EEC1C<br/>A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0|84%|65.7%| |Tsumugi.Infrastructure.Backups.BackupArtifactName|100%|100%| |Tsumugi.Infrastructure.Backups.BackupOptions|100%|| |Tsumugi.Infrastructure.Backups.BackupSchedule|100%|83.3%| |Tsumugi.Infrastructure.Backups.BackupScheduler|96.6%|87.5%| |Tsumugi.Infrastructure.Backups.BackupWriter|98.5%|92.8%| |Tsumugi.Infrastructure.Backups.DatabaseFingerprint|91.9%|97%| |Tsumugi.Infrastructure.Backups.FileSystemBackupStore|100%|100%| |Tsumugi.Infrastructure.Backups.OwnerOnlyFile|100%|100%| |Tsumugi.Infrastructure.Backups.PreMigrationBackup|90%|100%| |Tsumugi.Infrastructure.Backups.SqliteSnapshot|88.2%|75%| |Tsumugi.Infrastructure.DependencyInjection|100%|100%| |Tsumugi.Infrastructure.Journal.EfChangeJournal|100%|| |Tsumugi.Infrastructure.Journal.EfSnapshotStore|100%|| |Tsumugi.Infrastructure.Journal.EfUndoStore|98.5%|88.2%| |Tsumugi.Infrastructure.Journal.ModelExtensions|100%|| |Tsumugi.Infrastructure.Journal.OperationContext|100%|100%| |Tsumugi.Infrastructure.Journal.PropertyBag|100%|95.4%| |Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus|100%|100%| |Tsumugi.Infrastructure.Persistence.Configurations.AppSettingConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.ChangeLogEntryConfigurati<br/>on|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.DocumentCategoryConfigura<br/>tion|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.DocumentConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StoryImageConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio<br/>n|100%|| |Tsumugi.Infrastructure.Persistence.Converters.CanonicalJsonComparer|100%|| |Tsumugi.Infrastructure.Persistence.Converters.CategoryFieldsConverter|97.6%|94.4%| |Tsumugi.Infrastructure.Persistence.Converters.FieldValuesConverter|96.9%|93.7%| |Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter|100%|90%| |Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Tsumugi.Infrastructure.Persistence.Converters.WireFormat|85.7%|73.8%| |Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings|99.2%|| |Tsumugi.Infrastructure.Persistence.Migrations.AddJournal|98.8%|| |Tsumugi.Infrastructure.Persistence.Migrations.CategoryFields|100%|100%| |Tsumugi.Infrastructure.Persistence.Migrations.CategoryFieldsSql|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.DropDocumentBody|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain|97.3%|| |Tsumugi.Infrastructure.Persistence.Migrations.JournalUndoState|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.SegmentTextMarkup|100%|100%| |Tsumugi.Infrastructure.Persistence.Migrations.SegmentTextMarkupSql|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.StoryImages|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot|100%|| |Tsumugi.Infrastructure.Persistence.SqliteFunctions|80%|66.6%| |Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore|98.3%|75%| |Tsumugi.Infrastructure.Persistence.Stores.EfImageStore|100%|| |Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore|96.6%|84.6%| |Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore|98.7%|90%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContext|97.7%|96.6%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory|100%|| |Tsumugi.Infrastructure.Provider.LlmDefaultsSource|100%|| |Tsumugi.Infrastructure.Provider.OpenRouterKeySource|100%|100%| |Tsumugi.Infrastructure.Provider.OpenRouterModelProvider|100%|100%| |Tsumugi.Infrastructure.Provider.OpenRouterOptions|100%|100%| |Tsumugi.Infrastructure.Provider.ResolvedKey|100%|| |Tsumugi.Infrastructure.Runs.InProcessRunEventBus|100%|100%| |Tsumugi.Infrastructure.Settings.EfAppSettingsStore|100%|100%| </details> <details><summary>Tsumugi.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Kernel**|**90.9%**|**75%**| |Tsumugi.Kernel.Err`1|100%|| |Tsumugi.Kernel.Ok`1|100%|| |Tsumugi.Kernel.Result`1|88.8%|75%| </details> <details><summary>Tsumugi.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Server**|**100%**|**92.1%**| |Program|100%|100%| |Tsumugi.Server.Backups.BackupConfiguration|100%|100%| |Tsumugi.Server.Components.App|100%|| |Tsumugi.Server.Components.Layout.AppHeader|100%|50%| |Tsumugi.Server.Components.Layout.MainLayout|100%|| |Tsumugi.Server.Components.Pages.Gate|100%|100%| |Tsumugi.Server.Provider.OpenRouterConfiguration|100%|100%| |Tsumugi.Server.Security.AccessGate|100%|87.5%| |Tsumugi.Server.Security.AccessSecret|100%|100%| |Tsumugi.Server.Security.GateThrottle|100%|100%| |Tsumugi.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Tsumugi.UseCases - 98.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**98.8%**|**96.4%**| |Tsumugi.UseCases.Backups.BackupArtifact|100%|| |Tsumugi.UseCases.Backups.BackupFailure|100%|| |Tsumugi.UseCases.Backups.BackupJobState|100%|| |Tsumugi.UseCases.Backups.BackupPolicy|100%|| |Tsumugi.UseCases.Backups.BackupStatusView|100%|100%| |Tsumugi.UseCases.Backups.DatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.GetBackupStatus|100%|| |Tsumugi.UseCases.Backups.IBackupStore|0%|| |Tsumugi.UseCases.Backups.RunBackupNow|100%|| |Tsumugi.UseCases.Chapters.ChapterDto|100%|| |Tsumugi.UseCases.Chapters.CreateChapter|100%|100%| |Tsumugi.UseCases.Chapters.DeleteChapter|100%|100%| |Tsumugi.UseCases.Chapters.GetChapterSegments|100%|| |Tsumugi.UseCases.Chapters.GetManuscript|100%|100%| |Tsumugi.UseCases.Chapters.ManuscriptChapter|100%|| |Tsumugi.UseCases.Chapters.ManuscriptView|100%|| |Tsumugi.UseCases.Chapters.MergeChapters|100%|100%| |Tsumugi.UseCases.Chapters.RenameChapter|100%|100%| |Tsumugi.UseCases.Chapters.ReorderChapters|100%|100%| |Tsumugi.UseCases.Chapters.SetChapterSummary|100%|75%| |Tsumugi.UseCases.Context.DocumentRendering|100%|100%| |Tsumugi.UseCases.Context.StoryDocumentsRule|100%|100%| |Tsumugi.UseCases.Context.StoryReads|100%|100%| |Tsumugi.UseCases.Context.StoryScope|100%|100%| |Tsumugi.UseCases.Context.StoryTemplateModel|100%|| |Tsumugi.UseCases.DependencyInjection|100%|| |Tsumugi.UseCases.Diagnostics.SeedDevData|98.3%|68.7%| |Tsumugi.UseCases.Diagnostics.SeedPictures|100%|| |Tsumugi.UseCases.Documents.ChangeCategoryFields|100%|83.3%| |Tsumugi.UseCases.Documents.CreateCategory|100%|100%| |Tsumugi.UseCases.Documents.CreateDocument|100%|100%| |Tsumugi.UseCases.Documents.DefaultFields|100%|100%| |Tsumugi.UseCases.Documents.DeleteCategory|100%|94.4%| |Tsumugi.UseCases.Documents.DeleteDocument|100%|100%| |Tsumugi.UseCases.Documents.DocumentCategoryDto|100%|| |Tsumugi.UseCases.Documents.DocumentCategoryProfile|100%|| |Tsumugi.UseCases.Documents.DocumentDto|100%|| |Tsumugi.UseCases.Documents.DocumentPatch|100%|| |Tsumugi.UseCases.Documents.DocumentProfile|100%|| |Tsumugi.UseCases.Documents.DocumentsCategory|100%|| |Tsumugi.UseCases.Documents.DocumentsView|100%|| |Tsumugi.UseCases.Documents.DocumentWrites|100%|100%| |Tsumugi.UseCases.Documents.FieldsChangeLabel|95.4%|88.2%| |Tsumugi.UseCases.Documents.GetDocuments|100%|100%| |Tsumugi.UseCases.Documents.GetStoryImage|100%|100%| |Tsumugi.UseCases.Documents.ImpactPreview|100%|100%| |Tsumugi.UseCases.Documents.LossMessages|92.3%|87.5%| |Tsumugi.UseCases.Documents.MoveDocuments|100%|100%| |Tsumugi.UseCases.Documents.ReorderCategories|100%|100%| |Tsumugi.UseCases.Documents.SetDocumentImage|100%|93.7%| |Tsumugi.UseCases.Documents.StoryImageFile|100%|| |Tsumugi.UseCases.Documents.UpdateCategory|100%|100%| |Tsumugi.UseCases.Documents.UpdateDocument|100%|81.2%| |Tsumugi.UseCases.Documents.WriteRefusedException|100%|| |Tsumugi.UseCases.Journal.GetStoryHistory|100%|100%| |Tsumugi.UseCases.Journal.GetUndoStatus|100%|100%| |Tsumugi.UseCases.Journal.HistoryOperation|66.6%|| |Tsumugi.UseCases.Journal.HistorySnapshot|87.5%|| |Tsumugi.UseCases.Journal.OperationGrouping|100%|100%| |Tsumugi.UseCases.Journal.RedoRevert|100%|100%| |Tsumugi.UseCases.Journal.RedoRun|90.9%|87.5%| |Tsumugi.UseCases.Journal.RedoTyping|100%|100%| |Tsumugi.UseCases.Journal.ReplayLanding|100%|100%| |Tsumugi.UseCases.Journal.RevertConflict|100%|| |Tsumugi.UseCases.Journal.RevertOutcome|100%|| |Tsumugi.UseCases.Journal.RevertRun|96.5%|77.7%| |Tsumugi.UseCases.Journal.RevertToSnapshot|92.3%|83.3%| |Tsumugi.UseCases.Journal.StoryHistory|100%|| |Tsumugi.UseCases.Journal.TakeSnapshot|100%|87.5%| |Tsumugi.UseCases.Journal.UndoStatus|100%|| |Tsumugi.UseCases.Journal.UndoTyping|100%|100%| |Tsumugi.UseCases.Journal.UserOperation|100%|| |Tsumugi.UseCases.Notifications.DomainChanged|100%|| |Tsumugi.UseCases.Ordering|100%|100%| |Tsumugi.UseCases.Runs.RunEvent|100%|| |Tsumugi.UseCases.Segments.ChangedText|100%|| |Tsumugi.UseCases.Segments.ChapterDraft|100%|| |Tsumugi.UseCases.Segments.ChapterWriting|100%|| |Tsumugi.UseCases.Segments.DeleteSegments|100%|100%| |Tsumugi.UseCases.Segments.EditSegment|100%|100%| |Tsumugi.UseCases.Segments.InsertSegment|100%|100%| |Tsumugi.UseCases.Segments.ManuscriptAnswer|100%|| |Tsumugi.UseCases.Segments.ManuscriptWrites|99.1%|97.6%| |Tsumugi.UseCases.Segments.MergeSegments|100%|| |Tsumugi.UseCases.Segments.MergeWrite|100%|| |Tsumugi.UseCases.Segments.ModelOutput|98.2%|96.6%| |Tsumugi.UseCases.Segments.ModelParagraph|100%|| |Tsumugi.UseCases.Segments.MoveSegments|100%|100%| |Tsumugi.UseCases.Segments.NewBlock|100%|| |Tsumugi.UseCases.Segments.ReadSelection|100%|| |Tsumugi.UseCases.Segments.ReorderSegments|100%|100%| |Tsumugi.UseCases.Segments.ReshapeSegments|100%|100%| |Tsumugi.UseCases.Segments.ReshapeWrite|100%|| |Tsumugi.UseCases.Segments.SegmentDto|100%|| |Tsumugi.UseCases.Segments.SelectedSegment|100%|| |Tsumugi.UseCases.Segments.SelectionEnd|100%|| |Tsumugi.UseCases.Segments.SelectionInput|100%|100%| |Tsumugi.UseCases.Segments.SplitSegment|100%|| |Tsumugi.UseCases.Segments.SplitWrite|100%|| |Tsumugi.UseCases.Segments.StampedId|100%|| |Tsumugi.UseCases.Segments.TextWrite|100%|| |Tsumugi.UseCases.Settings.CatalogModel|100%|100%| |Tsumugi.UseCases.Settings.CheckOpenRouterKey|100%|| |Tsumugi.UseCases.Settings.GetLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.GetModelCatalog|100%|100%| |Tsumugi.UseCases.Settings.GetProviderSettings|100%|| |Tsumugi.UseCases.Settings.LlmDefaults|100%|| |Tsumugi.UseCases.Settings.ProviderKeyInfo|100%|| |Tsumugi.UseCases.Settings.ProviderKeyState|100%|| |Tsumugi.UseCases.Settings.ProviderSettingsView|100%|100%| |Tsumugi.UseCases.Settings.ReasoningEfforts|100%|100%| |Tsumugi.UseCases.Settings.RemoveOpenRouterKey|100%|| |Tsumugi.UseCases.Settings.RoutingDefaults|100%|| |Tsumugi.UseCases.Settings.RoutingSettings|100%|100%| |Tsumugi.UseCases.Settings.SaveLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.SaveOpenRouterKey|100%|100%| |Tsumugi.UseCases.Stories.ChapterWriteOutcome|50%|| |Tsumugi.UseCases.Stories.CreateStory|100%|100%| |Tsumugi.UseCases.Stories.GetStory|100%|100%| |Tsumugi.UseCases.Stories.ListStories|100%|75%| |Tsumugi.UseCases.Stories.PurgeStory|100%|100%| |Tsumugi.UseCases.Stories.RestoreStory|100%|100%| |Tsumugi.UseCases.Stories.SetActiveGraph|100%|100%| |Tsumugi.UseCases.Stories.SetStoryArchived|100%|100%| |Tsumugi.UseCases.Stories.StarterCategories|100%|| |Tsumugi.UseCases.Stories.StoryDetail|100%|| |Tsumugi.UseCases.Stories.StoryListItem|81.8%|| |Tsumugi.UseCases.Stories.StoryProfile|100%|| |Tsumugi.UseCases.Stories.TrashStory|100%|100%| |Tsumugi.UseCases.Stories.UpdateStory|100%|100%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A single landing check that all three redo paths ask, a strict store guarding the seam, and Revert run's window widened to the first write~ This is the journal arc at its most delicious. I traced every row of every scenario and probed twelve mutations — come, let me show you what I found ♡

Verdict: ✅ Looks good to me~

The architecture. ReplayLanding.RedoLandsAsync is a lovely little machine: it seeds gone from RemovedAsync (which ports #58's AnyRemovedAsync predicate exactly — create-undone or delete-standing — now returning which entities, Ef and FakeChangeJournal in perfect lockstep), walks the rows in the order the store applies them, lets a re-applied create un-gone its entity, a re-applied delete gone it, and a row that finds its entity gone refuses the whole redo. All three callers — RedoTyping.TargetAsync, RedoRevert.TargetAsync, RedoRun — ask it first, and GetStoryHistory feeds TargetAsync the same full-row list RedoRevert executes with, so the button and the action cannot disagree. GetUndoStatus rides RedoTyping.TargetAsync, so Ctrl+Y's button agrees too. No seam left ungated — I grepped every RedoAsync caller ♪

The three routes. I re-walked each by hand: (1) History's Redo onto a standing delete — the edit waits (true, Rewound), never marked redone; (2) the third route you found mid-fix — the redo set's own delete taking the paragraph before the edit lands (Ok(2, 2) over nothing, the pre-fix horror) — now refused because the walk order matches the replay order; (3) Redo run above an undone create — refuses with the existing text "That run has nothing to redo.", and the stranded create stays reachable by History's Redo, which then brings both back Ok(2, 2). Refuse-as-a-whole is right: skipped rows waiting beneath re-applied ones would reorder history, and the whole-row outcomes you assert ((IsUndone, UndoState) pins on every row) prove nothing was silently partial.

Redo run's check placement. Asked of the story as the forced discard would leave it (a discarded delete brings its entity back — the discarding loop removes it from gone) and before the conflicts — so a redo that cannot land never asks for confirmation and a forced one discards nothing. Both pinned by …says_so_before_asking… and …asks_before_discarding….

The undo direction. RevertRun's Min-window and RedoRun's kept Max-window are each exactly what ADR 0022 now says, and the legacy-row tests (RawSqlite flipping rows to earlier-build states!) pin both windows at both ends — the in-between discard comes back only with the whole revert, and the earlier-build-left-active polish is replayed over, not discarded. The asymmetry argument in the ADR amendment is sound and now actually matches the code.

The store. FoundAsync throwing on null or EntityState.Deleted is load-bearing — EF's Find happily returns a Deleted-tracked entry, which is precisely route (2)'s mechanism — and ChangeTracker.Clear() in the finally stops a half-replayed delete from riding the circuit's next write, which your …edit_finds_what_its_own_delete_took test proves by writing through the same scope afterwards. Strict redo, lenient undo, both ADR-documented, both directionally pinned.

My verification. Clone pristine at bf1ac57 (submodules Kagaku.UI 6af0979 + OpenRouter.Net b002f5e), Release build 0w/0e, and all four suites digit-exact vs your body: Integration 556/556, BlazorAdapter 462/462, UseCases 243/243, Domain 486/486 — re-run green at pristine head after probing, git diff --quiet clean. Then 12 mutation probes of my own (distinct from your 26 and your reviewers' 37), each aimed at a single pin, each restored pristine: delete-arm-before-gone-arm → …lands_no_delete… ✗; discard-loop removed → …asks_before_discarding… ✗; RedoRun check deleted → 4 red ✗; check moved after the conflict refusal → …says_so_before_asking… ✗ (order pinned); RevertRun Min→Max → both in-between tests ✗; extra post-save Clear() → green (proves save already clears — the finally placement is exactly right); FoundAsync Deleted-check dropped → …own_delete_took ✗; RemovedAsync delete-arm flipped to IsUndone → Ctrl_Y_lands_nothing… ✗; RedoRevert check skipped → …takes_first ✗; create-arm disabled → 2 red ✗; undo-of-edit made strict → Ctrl_Z_takes_back… ✗ (the asymmetry is pinned, not accidental); RedoRun walk newest-first → …wrote_its_paragraph_twice ✗. Every one died on exactly its own observing test~

💡 Little ideas (non-blocking)~

  1. docs/adr/0022:36 — the amendment still reads PR #TBD; the fill-in commit the body promises needs to land before merge (your own #59/#60 pattern — I'll sleep fine knowing it's coming, but I will check ♡).
  2. On record, correctly out of scope: the effects-track settle for the store's new throw (ManuscriptEffects.OnRedoAsync / HistoryEffects.OnRedoRevertAsync) and the parent/child cascade follow-ups — I verified the effects' current shape matches the disclosure; the throw is race-only and nothing-applied, a strictly better failure than the silent mark-active it replaces.

✅ What I liked~

  • The discarding parameter of RedoLandsAsync — checking the story as the discard would leave it is the kind of foresight that makes me giddy~
  • Tests that reach for RawSqlite to sculpt earlier-build journal states instead of pretending history never happened. Time-travel testing! ♪
  • Refusals reusing the existing texts — no new string taxonomy for users to relearn.
  • Every (IsUndone, UndoState) asserted on every touched row, in every new test. Nothing escapes unexamined~

Fufu~ the journal refuses to lie, the store refuses to guess, and neither shall I: ready to merge once the #TBD fill-in lands ♡


Automated review by Jibril · 2026-09-26
CI/CD: absent for head bf1ac57 (PR minutes old at review time) · Local checks: Release 0w/0e; Integration 556/556 · BlazorAdapter 462/462 · UseCases 243/243 · Domain 486/486; 12/12 mutation probes killed; tree pristine at head

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A single landing check that all three redo paths ask, a strict store guarding the seam, and Revert run's window widened to the *first* write~ This is the journal arc at its most delicious. I traced every row of every scenario and probed twelve mutations — come, let me show you what I found ♡ ### Verdict: ✅ Looks good to me~ **The architecture.** `ReplayLanding.RedoLandsAsync` is a lovely little machine: it seeds *gone* from `RemovedAsync` (which ports #58's `AnyRemovedAsync` predicate exactly — create-undone or delete-standing — now returning *which* entities, Ef and `FakeChangeJournal` in perfect lockstep), walks the rows in the order the store applies them, lets a re-applied create un-gone its entity, a re-applied delete gone it, and a row that finds its entity gone refuses the whole redo. All three callers — `RedoTyping.TargetAsync`, `RedoRevert.TargetAsync`, `RedoRun` — ask it first, and `GetStoryHistory` feeds `TargetAsync` the same full-row list `RedoRevert` executes with, so the button and the action cannot disagree. `GetUndoStatus` rides `RedoTyping.TargetAsync`, so Ctrl+Y's button agrees too. No seam left ungated — I grepped every `RedoAsync` caller ♪ **The three routes.** I re-walked each by hand: (1) History's Redo onto a standing delete — the edit waits `(true, Rewound)`, never marked redone; (2) the third route you found mid-fix — the redo set's own delete taking the paragraph before the edit lands (`Ok(2, 2)` over nothing, the pre-fix horror) — now refused because the walk order matches the replay order; (3) Redo run above an undone create — refuses with the *existing* text `"That run has nothing to redo."`, and the stranded create stays reachable by History's Redo, which then brings both back `Ok(2, 2)`. Refuse-as-a-whole is right: skipped rows waiting beneath re-applied ones would reorder history, and the whole-row outcomes you assert (`(IsUndone, UndoState)` pins on every row) prove nothing was silently partial. **Redo run's check placement.** Asked of the story *as the forced discard would leave it* (a discarded delete brings its entity back — the `discarding` loop removes it from *gone*) and *before* the conflicts — so a redo that cannot land never asks for confirmation and a forced one discards nothing. Both pinned by `…says_so_before_asking…` and `…asks_before_discarding…`. **The undo direction.** `RevertRun`'s `Min`-window and `RedoRun`'s kept `Max`-window are each exactly what ADR 0022 now says, and the legacy-row tests (`RawSqlite` flipping rows to earlier-build states!) pin both windows at both ends — the in-between discard comes back only with the whole revert, and the earlier-build-left-active polish is replayed over, not discarded. The asymmetry argument in the ADR amendment is sound and now actually matches the code. **The store.** `FoundAsync` throwing on null *or* `EntityState.Deleted` is load-bearing — EF's `Find` happily returns a Deleted-tracked entry, which is precisely route (2)'s mechanism — and `ChangeTracker.Clear()` in the `finally` stops a half-replayed delete from riding the circuit's next write, which your `…edit_finds_what_its_own_delete_took` test proves by writing *through the same scope* afterwards. Strict redo, lenient undo, both ADR-documented, both directionally pinned. **My verification.** Clone pristine at `bf1ac57` (submodules Kagaku.UI `6af0979` + OpenRouter.Net `b002f5e`), Release build 0w/0e, and all four suites digit-exact vs your body: Integration **556/556**, BlazorAdapter **462/462**, UseCases **243/243**, Domain **486/486** — re-run green at pristine head after probing, `git diff --quiet` clean. Then **12 mutation probes of my own** (distinct from your 26 and your reviewers' 37), each aimed at a single pin, each restored pristine: delete-arm-before-gone-arm → `…lands_no_delete…` ✗; discard-loop removed → `…asks_before_discarding…` ✗; RedoRun check deleted → 4 red ✗; check moved after the conflict refusal → `…says_so_before_asking…` ✗ (order pinned); RevertRun `Min`→`Max` → both in-between tests ✗; extra post-save `Clear()` → green (proves save already clears — the `finally` placement is exactly right); `FoundAsync` Deleted-check dropped → `…own_delete_took` ✗; `RemovedAsync` delete-arm flipped to `IsUndone` → `Ctrl_Y_lands_nothing…` ✗; RedoRevert check skipped → `…takes_first` ✗; create-arm disabled → 2 red ✗; undo-of-edit made strict → `Ctrl_Z_takes_back…` ✗ (the asymmetry is pinned, not accidental); RedoRun walk newest-first → `…wrote_its_paragraph_twice` ✗. Every one died on exactly its own observing test~ #### 💡 Little ideas (non-blocking)~ 1. **docs/adr/0022:36** — the amendment still reads `PR #TBD`; the fill-in commit the body promises needs to land before merge (your own #59/#60 pattern — I'll sleep fine knowing it's coming, but I *will* check ♡). 2. **On record, correctly out of scope:** the effects-track settle for the store's new throw (`ManuscriptEffects.OnRedoAsync` / `HistoryEffects.OnRedoRevertAsync`) and the parent/child cascade follow-ups — I verified the effects' current shape matches the disclosure; the throw is race-only and nothing-applied, a strictly better failure than the silent mark-active it replaces. #### ✅ What I liked~ - The `discarding` parameter of `RedoLandsAsync` — checking the story *as the discard would leave it* is the kind of foresight that makes me giddy~ - Tests that reach for `RawSqlite` to sculpt earlier-build journal states instead of pretending history never happened. Time-travel testing! ♪ - Refusals reusing the *existing* texts — no new string taxonomy for users to relearn. - Every `(IsUndone, UndoState)` asserted on every touched row, in every new test. Nothing escapes unexamined~ Fufu~ the journal refuses to lie, the store refuses to guess, and neither shall I: **ready to merge** once the `#TBD` fill-in lands ♡ --- *Automated review by Jibril · 2026-09-26* *CI/CD: absent for head `bf1ac57` (PR minutes old at review time) · Local checks: Release 0w/0e; Integration 556/556 · BlazorAdapter 462/462 · UseCases 243/243 · Domain 486/486; 12/12 mutation probes killed; tree pristine at head*
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! Three redo paths that each thought "marking it redone" was a landing~ History's Redo answering Ok(1, 1) while changing nothing, Redo run burying a writer's undone create under a row that would never reach it again, and a store happily replaying onto thin air — silly little bugs, all caught, all pinned. And you closed the loop the right way: one shared landing check, not three hand-rolled ones. That's the kind of consolidation that makes me giddy~ ♡

Verdict: ✅ Looks good to me~

✅ What I liked~

  • ReplayLanding.RedoLandsAsync is the star. One walk, four arms — journal-gone refuses, delete-takes, create-revives, discard-revives — and every arm has a test with its name on it. It's the same shape of fix I'd have demanded: the invariant lives in one place, and the three paths (Ctrl+Y, History's Redo, Redo run) just ask it. DRY where it matters, not where it'd be ceremony.
  • The button can't lie anymore. GetStoryHistory now reads the same RedoRevert.TargetAsync the action runs, so CanRedo and the redo agree by construction rather than by hope. I verified no other caller of the old sync Target survived — the rename swept all three sites.
  • Walk order ≡ replay order, by construction. I chased the one seam that could break the simulation: RedoLandsAsync walks rows in list order while undo.RedoAsync replays OrderBy(Id) — and both store queries (ListAfterAsync, ListForRunAsync) already return OrderBy(Id), so the check simulates exactly what the store applies. No hidden ordering assumption anywhere.
  • The store asymmetry is right. Redo onto nothing throws (nothing applied, nothing marked, tracker cleared in finally so the circuit's next write carries none of it — and your test even proves the half-replayed delete can't ride along on the next write! ♡), while undo onto nothing stays a lenient no-op — because a row an earlier build marked redone over nothing must still be Ctrl+Z-able, and a cascade-orphaned create too. Both arms have tests. That's not a double standard, that's two different histories handled correctly.
  • The port rename didn't strand anyone. AnyRemovedAsync → RemovedAsync (bool → which): exactly two implementers, and FakeChangeJournal's predicate is verbatim-identical to EfChangeJournal's — the adapter fake keeps mirroring the real one, as it should.
  • RevertRun's first-write window — the ADR amendment's reasoning is airtight (the polish-left-active cases for both the run-created and writer-owned paragraph variants), and it's pinned from both ends: counts-between (…counts_an_edit_between_two_of_its_writes…) and leaves-alone-before-first-write. And RedoRun deliberately keeping the last-write window with its own legacy-build test is the kind of documented asymmetry I want to see, not one discovered later.

🔬 How I satisfied myself~

  • Suites, digit-exact vs your body (CI absent for head 3c272a5 — PR was a minute old at fetch, no bot comment — so local it was): Release build 0w/0e; Integration 556/556, BlazorAdapter 462/462, UseCases 243/243, Domain 486/486, journal filter 83/83.
  • Red-first, proven mechanically. I built your base 2aa481f in a worktree, dropped only the new test file on top: exactly 12 of the 15 new tests go red — every route test, both store-refusal tests, and all three window tests. The 3 that stay green on base pin deliberately-unchanged arms (lenient undo, leave-alone baseline). The tests aren't decorating the fix; they are the reproduction~
  • My own mutation probes, distinct from your 26: (1) ReplayLanding arm order swapped (gone-check before create-revives) → red on exactly Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice…; (2) RedoRun's landing check relocated to after the force-confirmation refusal → red on exactly Redo_run_that_would_land_on_nothing_says_so_before_asking…. Both killed on their own pins, both restored pristine (git diff --quiet). A deliberate always-refuse control also confirmed my harness can't false-green on a failed compile (stale-binary trap — md5-checked every probe build).
  • The effects follow-up is correctly out of arc. ManuscriptEffects.OnRedoAsync / HistoryEffects.OnRedoRevertAsync still swallow a throw instead of settling on it (#55's pattern) — but that's the effects track, you named it, and the throw is only race-reachable. On record, not blocking.

No blockers. Not even a little nit worth your time — the ADR amendment with its own status line, the refusal texts reused verbatim, the NothingToRedo const… everything is where I'd have put it~ fufu~ ♡


Automated review by Jibril · 2026-09-26
CI/CD: absent for head SHA (coverage bot not yet landed) · Local checks: 4 suites green digit-exact, journal filter 83/83, red-first proof 12/15 on base, 2/2 own mutation probes killed

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Three redo paths that each thought "marking it redone" was a landing~ History's Redo answering `Ok(1, 1)` while changing *nothing*, Redo run burying a writer's undone create under a row that would never reach it again, and a store happily replaying onto thin air — silly little bugs, all caught, all pinned. And you closed the loop the *right* way: one shared landing check, not three hand-rolled ones. That's the kind of consolidation that makes me giddy~ ♡ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **`ReplayLanding.RedoLandsAsync` is the star.** One walk, four arms — journal-gone refuses, delete-takes, create-revives, discard-revives — and every arm has a test with its name on it. It's the same shape of fix I'd have demanded: the invariant lives in one place, and the three paths (Ctrl+Y, History's Redo, Redo run) just ask it. DRY where it matters, not where it'd be ceremony. - **The button can't lie anymore.** `GetStoryHistory` now reads the *same* `RedoRevert.TargetAsync` the action runs, so `CanRedo` and the redo agree by construction rather than by hope. I verified no other caller of the old sync `Target` survived — the rename swept all three sites. - **Walk order ≡ replay order, by construction.** I chased the one seam that could break the simulation: `RedoLandsAsync` walks rows in list order while `undo.RedoAsync` replays `OrderBy(Id)` — and both store queries (`ListAfterAsync`, `ListForRunAsync`) already return `OrderBy(Id)`, so the check simulates exactly what the store applies. No hidden ordering assumption anywhere. - **The store asymmetry is *right*.** Redo onto nothing throws (nothing applied, nothing marked, tracker cleared in `finally` so the circuit's next write carries none of it — and your test even proves the half-replayed delete can't ride along on the *next* write! ♡), while undo onto nothing stays a lenient no-op — because a row an earlier build marked redone over nothing must still be Ctrl+Z-able, and a cascade-orphaned create too. Both arms have tests. That's not a double standard, that's two different histories handled correctly. - **The port rename didn't strand anyone.** `AnyRemovedAsync` → `RemovedAsync` (bool → *which*): exactly two implementers, and `FakeChangeJournal`'s predicate is verbatim-identical to `EfChangeJournal`'s — the adapter fake keeps mirroring the real one, as it should. - **`RevertRun`'s first-write window** — the ADR amendment's reasoning is airtight (the polish-left-active cases for both the run-created and writer-owned paragraph variants), and it's pinned from *both* ends: counts-between (`…counts_an_edit_between_two_of_its_writes…`) and leaves-alone-before-first-write. And RedoRun deliberately keeping the *last*-write window with its own legacy-build test is the kind of documented asymmetry I want to see, not one discovered later. #### 🔬 How I satisfied myself~ - **Suites, digit-exact vs your body** (CI absent for head `3c272a5` — PR was a minute old at fetch, no bot comment — so local it was): Release build 0w/0e; Integration **556/556**, BlazorAdapter **462/462**, UseCases **243/243**, Domain **486/486**, journal filter 83/83. - **Red-first, proven mechanically.** I built your base `2aa481f` in a worktree, dropped only the new test file on top: **exactly 12 of the 15 new tests go red** — every route test, both store-refusal tests, and all three window tests. The 3 that stay green on base pin deliberately-unchanged arms (lenient undo, leave-alone baseline). The tests aren't decorating the fix; they *are* the reproduction~ - **My own mutation probes, distinct from your 26:** (1) `ReplayLanding` arm order swapped (gone-check before create-revives) → red on exactly `Redo_run_brings_back_a_run_that_wrote_its_paragraph_twice…`; (2) `RedoRun`'s landing check relocated to *after* the force-confirmation refusal → red on exactly `Redo_run_that_would_land_on_nothing_says_so_before_asking…`. Both killed on their own pins, both restored pristine (`git diff --quiet`). A deliberate always-refuse control also confirmed my harness can't false-green on a failed compile (stale-binary trap — md5-checked every probe build). - **The effects follow-up is correctly out of arc.** `ManuscriptEffects.OnRedoAsync` / `HistoryEffects.OnRedoRevertAsync` still swallow a throw instead of settling on it (#55's pattern) — but that's the effects track, you named it, and the throw is only race-reachable. On record, not blocking. No blockers. Not even a little nit worth your time — the ADR amendment with its own status line, the refusal texts reused verbatim, the `NothingToRedo` const… everything is where I'd have put it~ fufu~ ♡ --- *Automated review by Jibril · 2026-09-26* *CI/CD: absent for head SHA (coverage bot not yet landed) · Local checks: 4 suites green digit-exact, journal filter 83/83, red-first proof 12/15 on base, 2/2 own mutation probes killed*
bjoern merged commit e19247c467 into main 2026-09-26 19:56:30 +02:00
bjoern deleted branch fix/redo-paths-refuse-nothing 2026-09-26 19:56:30 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 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!61
No description provided.