fix(journal): redo that stays cleared — a new operation clears its story's waiting rows for good, Ctrl+Y replays nothing past a revert to an older version, and the journal records how each row was undone #56

Merged
bjoern merged 2 commits from fix/redo-discarded-rows into main 2026-09-26 17:39:55 +02:00
Member

Summary

Two redo bugs remained after #44. Both come from working redo out of journal position alone: "R undone, then S written, then S undone" and "R and S written, S undone, then R undone" leave the same undone rows in the same order, so no positional rule can tell them apart (#44's attempt was blocked for that reason). The journal now records, per undone row, what it waits for.

  • Bug 1 — a cleared redo set came back. Split 'Hello world' → Ctrl+Z → type '!' → Ctrl+Z → Ctrl+Y → Ctrl+Y re-applied the split. Through History: Revert to here → a new edit → Ctrl+Z → History's Redo brought the reverted operations back. Now a new operation clears the redo set for good: undoing it brings back that operation and nothing it cleared, by Ctrl+Y and by History's Redo.
  • Bug 2 — Ctrl+Y after Revert to here on an older snapshot. Ctrl+Y looked only above the newest snapshot, so it replayed the edit made after the newer version onto the reverted story while the operations between the versions stayed undone. Now Ctrl+Y answers "Nothing to redo."; History's Redo brings the revert back whole. The same holds with a run between the two versions, and after a forced run revert of the newest edit (Ctrl+Y would polish a paragraph that is not there).
  • ChangeLog.UndoState (migration 20260926134739_JournalUndoState: a nullable INTEGER plus an index on (StoryId, UndoState), no table rebuild):
    • Rewound (1): undone with no active row left above it — Ctrl+Z, Revert to here, a run revert with nothing after it.
    • CutOut (2): undone from under a later active row — a run revert, a run redo's forced discard.
    • Cleared (3): a new operation was saved while the row was undone.
    • EfUndoStore marks each undone row against its story's newest row still active. ChangeLogEntry.MarkUndone(bool cutOut = false): an undo that does not say is marked Rewound, the side where Ctrl+Y refuses.
  • Clearing runs in the new operation's own transaction on both save paths (TsumugiDbContext): per story, every undone row from the lowest Rewound/CutOut row up is marked Cleared. The SQL is one UPDATE whose bound is a scalar subquery; SQLite plans it as SEARCH USING INDEX IX_ChangeLog_StoryId_Id (StoryId=? AND Id>?) with SEARCH USING COVERING INDEX IX_ChangeLog_StoryId_UndoState for the bound, so a save with nothing waiting costs two index probes.
  • Readers. RedoTyping (Ctrl+Y) takes the newest active row over the whole story (new IChangeJournal.LatestActivePositionAsync, a MAX), skips cleared rows, refuses while a Rewound row waits at or below the newest snapshot, and steps over a CutOut one there — so ADR 0022's PR #25 rule holds (Ctrl+Y re-applies a forced revert's field change once Ctrl+Z has made it whole). RedoRevert (History's Redo, and so GetStoryHistory.CanRedo) skips cleared rows. RedoRun is unchanged: it stays run-scoped and still brings a cleared run back under its conflict check.
  • Rows undone before the migration carry no state — nothing is backfilled — and keep the old build's behaviour: they redo by position, and a new operation clears one only with a row undone since that sits below it, whose redo would have to come first. Without that, an old undone paragraph above a chapter the new build undid and cleared stayed redoable, and Ctrl+Y and History's Redo threw a foreign-key DbUpdateException re-creating it in the deleted chapter (found in review, below).
  • ADR 0022 amended ("Amended 2026-09-26 (PR #56, the redo set)"): the Status line, the journal's column list, Revert run's discarded edits (redoable until a new operation clears them; below the run's snapshot, by History's Redo or by Ctrl+Y after Redo run), the Redo bullet (the states, why position alone cannot carry them, why a state per row rather than an "undone by" reference, the old rows' rule), and the Ctrl+Z/Ctrl+Y bullet.

Everything touched is inside the arc's paths: Domain/Journal, UseCases/Journal, Infrastructure/Journal, TsumugiDbContext, ChangeLogEntryConfiguration, one new migration plus the model snapshot, ADR 0022, and the journal tests. No seed change: the arc adds no user-authored content. No other migration landed on main since the branch; the branch is rebased on b0e8bff (#52, the manuscript hardening, which touches none of these files).

Verification

  • Suites (on b0e8bff, modules rebuilt): Integration 530/530, BlazorAdapter 415/415, Domain 486/486, UseCases 236/236. The journal-related filter (HistoryAndRevert, FieldJournal, Journal, every migration test, ImageField, AppSettings, JournalCompatibility — 97 tests) passed 10 runs in a row; the full Integration suite passed 3 runs; the adapter's History, ChapterPage and ManuscriptEffects tests (70) pass. The Kagaku.Flow suites were not run: nothing here touches them.
  • New tests (21 — 17 Integration, 4 Domain):
    • HistoryAndRevertTests: both repro sequences — Ctrl_Z_of_a_new_edit_brings_back_that_edit_and_never_the_split_the_edit_cleared (Ctrl+Y, with a neighbour story whose own undone rename waits below) and Ctrl_Z_of_the_edit_that_cleared_a_revert_leaves_History_s_Redo_that_edit_alone; bug 2's two-snapshot case, Revert_to_here_on_an_older_snapshot_leaves_Ctrl_Y_nothing_and_History_s_Redo_the_whole_revert (×2: with and without a Ctrl+Z first, another story written between); Revert_to_the_version_before_a_run_leaves_Ctrl_Y_nothing_to_replay_onto_the_story_without_it; A_new_edit_clears_a_run_revert_from_History_s_Redo_and_Redo_run_still_brings_the_run_back; A_forced_run_revert_of_the_newest_edit_leaves_Ctrl_Y_nothing_until_the_run_is_back (×2: History's Redo (2,2), or Redo run then Ctrl+Y (1,1)). The #44 round trip stays pinned by the existing A_version_saved_over_an_undone_edit_leaves_Ctrl_Y_the_edits_made_after_it.
    • FieldJournalTests.A_new_operation_clears_the_row_the_forced_revert_discarded_and_redo_brings_back_only_what_Ctrl_Z_took (×2); the PR #25 rule stays pinned by the existing Once_Ctrl_Z_has_made_the_field_change_whole_Ctrl_Y_re_applies_all_of_it_and_the_run_stays_reverted.
    • JournalTests.A_new_operation_clears_the_storys_waiting_rows_in_its_own_save_synchronous_or_not_and_a_failed_save_clears_nothing.
    • JournalUndoStateMigrationTests (new; the world written through the use cases, walked down to SegmentTextMarkup and up): the column comes in empty and a walk down gives the old build its ChangeLog byte for byte; Up adds a column and an index without rebuilding a table; a pre-upgrade row with nothing waiting below it redoes by position and a new operation leaves it alone, while a neighbour story's waiting row below it stays the neighbour's; a version saved over a pre-upgrade undone row leaves Ctrl+Y the edits made after it; a pre-upgrade row above one a new operation clears is cleared with it (×2: Ctrl+Y and History's Redo give (1,1), the old paragraph stays gone, both redos then refuse). The stored numbers are read raw (1 Rewound, 3 Cleared).
    • ChangeLogEntryTests (Domain, 4): a new row waits for nothing; an undone row is rewound unless cut out; a redone row waits for nothing again; the three states keep the numbers the journal stores (1, 2, 3).
  • Mutation probes — every one compiled and restored its file on every exit (a trap), and the file was touched:
    • The implementation's 27 probes were each killed by a named test.
    • This round's 17 were all killed:
      • Clearing (TsumugiDbContext.Waiting): HEAD's rule restored, old rows never cleared → the FK repro Theory (both cases); >= bottom → > → 8 tests incl. the split test; no bottom (every undone row cleared) → the nothing-waiting-below migration test, the version-over migration test and CategoryFieldsMigrationTests' byte-for-byte walk; IsUndone dropped (active rows marked) → the FK repro Theory's own-row assertion; the bound's story filter dropped → the nothing-waiting-below test's neighbour; the outer story filter dropped → the split test's neighbour; the bound only Rewound → the run-revert and FieldJournal History tests; only CutOut → 6 tests; the sync clear removed → the JournalTests sync save; the async clear removed → 8 tests.
      • RedoTyping: a pre-upgrade row below the snapshot blocking Ctrl+Y as a rewound one does (the review's R4) → the new version-over migration test; the below-snapshot check disabled → 5 tests incl. the new forced-revert Theory.
      • UndoState: Rewound = 1 renumbered to 4 (the review's E1) → the Domain numbers test and 3 migration tests.
      • MarkUndone's default flipped to CutOut → the Domain test and 10 Integration tests.
      • EfUndoStore: the cut-out comparison inverted → 10 tests; every undo rewound → the PR #25 FieldJournal test; every undo cut out → 9 tests.
    • Equivalent and left alone (both found by the review): Down's DropIndex (SQLite's DropColumn rebuilds the table without the index anyway) and HasConversion<int>() (EF maps enums to int by default; kept for the file's idiom). A != Cleared guard in the clearing query was an equivalent too (a cleared row set to cleared again) and is gone.
  • Query plan: the clearing UPDATE's WHERE read through EXPLAIN QUERY PLAN on SQLite: SEARCH c USING INDEX IX_ChangeLog_StoryId_Id (StoryId=? AND Id>?), bound by SCALAR SUBQUERY → SEARCH c0 USING COVERING INDEX IX_ChangeLog_StoryId_UndoState (StoryId=? AND UndoState=?).
  • Independent review: two reviewers read the change; one also ran 36 compiling probes (31 killed, 2 equivalents, 2 survivors) and three clean full-suite reruns. What they found, and what was done:
    • Major — a pre-upgrade undone row above a cleared one threw. Before the upgrade: a chapter, a paragraph in it, the paragraph undone. After it: Ctrl+Z the chapter, a new paragraph elsewhere, its Ctrl+Z. The old paragraph's row (no state) was left waiting while the chapter's row was cleared, so Ctrl+Y and History's Redo both threw SQLite Error 19: FOREIGN KEY constraint failed, and kept throwing after every later "new edit, Ctrl+Z". Fixed: clearing takes every undone row from the story's lowest rewound or cut-out row up, so an old row above a cleared one goes with it. Pinned by the FK repro Theory; HEAD's rule restored fails it.
    • Major — survivor R4. Nothing pinned that a pre-upgrade row below the newest snapshot is stepped over like a cut-out one. Pinned by A_version_saved_over_a_row_undone_before_the_upgrade_leaves_Ctrl_Y_the_edits_made_after_it; R4 now fails it.
    • Minor — survivor E1. The stored integers were not pinned. Now a Domain test pins 1, 2, 3 and the migration tests read the raw column.
    • Minor — old undone rows are never cleared, so bug 1 stays live for them, and the stated reason ("nothing guesses") did not hold for clearing. Partly taken: rows above a cleared one are now cleared (above). Clearing every pre-upgrade undone row at the next operation breaks CategoryFieldsMigrationTests.A_walk_down_and_up_keeps_every_bag_byte_for_byte…, which dumps the whole ChangeLog across a walk that drops the column by design; that file is outside this arc, so it is a follow-up. ADR 0022, UndoState and the migration summary now give the real rule: rows undone before the change keep the old build's behaviour.
    • Minor — the ADR said what, not why. The Redo bullet now says position alone cannot carry the states (the two histories leave the same rows undone in the same order) and why a state per row rather than an "undone by" reference (read in strict undo order, it would refuse Ctrl+Y of a forced revert's operation once Ctrl+Z has made it whole — PR #25).
    • Nit — "undone before 2026-09-26". Now "undone before the journal recorded its state (the JournalUndoState migration)": the upgrade, not the calendar.
    • Nit — Revert run still promised redoable discarded edits. Qualified: redoable until a new operation clears them; while the run's rows wait below its snapshot, by History's Redo, or by Ctrl+Y once Redo run has brought the run back. Pinned by the new forced-revert Theory.
    • Nit — MarkUndone's default was the permissive CutOut. Now MarkUndone(bool cutOut = false): an undo that does not say is marked Rewound, where Ctrl+Y refuses. Making it required would break the adapter's FakeUndoStore, outside this arc (follow-up); the fake now models Ctrl+Z as rewound.
    • Nit — the commit said the EfUndoStore path was closed. Reworded: bug 2's route to it is closed, but a cut-out discarded edit over an entity a reverted run created still reaches it (follow-up).

Follow-ups (outside this arc's paths):

  • Clear every pre-upgrade undone row at the next operation: CategoryFieldsMigrationTests' walk should compare its ChangeLog dump without UndoState, which the walk drops by design.
  • Make MarkUndone's cutOut required once the adapter's FakeUndoStore passes it.
  • EfUndoStore marks a replayed update active when its entity is missing (reachable through a cut-out discarded edit over an entity a reverted run created).
  • History lists a cleared operation as plain "undone" (HistoryOperation.IsUndone); showing it as cleared is a UI choice for later.

🤖 Generated with Claude Code

## Summary Two redo bugs remained after #44. Both come from working redo out of journal position alone: "R undone, then S written, then S undone" and "R and S written, S undone, then R undone" leave the same undone rows in the same order, so no positional rule can tell them apart (#44's attempt was blocked for that reason). The journal now records, per undone row, what it waits for. - **Bug 1 — a cleared redo set came back.** Split 'Hello world' → Ctrl+Z → type '!' → Ctrl+Z → Ctrl+Y → Ctrl+Y re-applied the split. Through History: Revert to here → a new edit → Ctrl+Z → History's Redo brought the reverted operations back. Now a new operation clears the redo set for good: undoing it brings back that operation and nothing it cleared, by Ctrl+Y and by History's Redo. - **Bug 2 — Ctrl+Y after Revert to here on an older snapshot.** Ctrl+Y looked only above the newest snapshot, so it replayed the edit made after the newer version onto the reverted story while the operations between the versions stayed undone. Now Ctrl+Y answers "Nothing to redo."; History's Redo brings the revert back whole. The same holds with a run between the two versions, and after a forced run revert of the newest edit (Ctrl+Y would polish a paragraph that is not there). - **`ChangeLog.UndoState`** (migration `20260926134739_JournalUndoState`: a nullable INTEGER plus an index on `(StoryId, UndoState)`, no table rebuild): - `Rewound` (1): undone with no active row left above it — Ctrl+Z, Revert to here, a run revert with nothing after it. - `CutOut` (2): undone from under a later active row — a run revert, a run redo's forced discard. - `Cleared` (3): a new operation was saved while the row was undone. - `EfUndoStore` marks each undone row against its story's newest row still active. `ChangeLogEntry.MarkUndone(bool cutOut = false)`: an undo that does not say is marked `Rewound`, the side where Ctrl+Y refuses. - **Clearing** runs in the new operation's own transaction on both save paths (`TsumugiDbContext`): per story, every undone row from the lowest `Rewound`/`CutOut` row up is marked `Cleared`. The SQL is one `UPDATE` whose bound is a scalar subquery; SQLite plans it as `SEARCH USING INDEX IX_ChangeLog_StoryId_Id (StoryId=? AND Id>?)` with `SEARCH USING COVERING INDEX IX_ChangeLog_StoryId_UndoState` for the bound, so a save with nothing waiting costs two index probes. - **Readers.** `RedoTyping` (Ctrl+Y) takes the newest active row over the whole story (new `IChangeJournal.LatestActivePositionAsync`, a MAX), skips cleared rows, refuses while a `Rewound` row waits at or below the newest snapshot, and steps over a `CutOut` one there — so ADR 0022's PR #25 rule holds (Ctrl+Y re-applies a forced revert's field change once Ctrl+Z has made it whole). `RedoRevert` (History's Redo, and so `GetStoryHistory.CanRedo`) skips cleared rows. `RedoRun` is unchanged: it stays run-scoped and still brings a cleared run back under its conflict check. - **Rows undone before the migration** carry no state — nothing is backfilled — and keep the old build's behaviour: they redo by position, and a new operation clears one only with a row undone since that sits below it, whose redo would have to come first. Without that, an old undone paragraph above a chapter the new build undid and cleared stayed redoable, and Ctrl+Y and History's Redo threw a foreign-key `DbUpdateException` re-creating it in the deleted chapter (found in review, below). - **ADR 0022 amended** ("Amended 2026-09-26 (PR #56, the redo set)"): the Status line, the journal's column list, Revert run's discarded edits (redoable until a new operation clears them; below the run's snapshot, by History's Redo or by Ctrl+Y after Redo run), the Redo bullet (the states, why position alone cannot carry them, why a state per row rather than an "undone by" reference, the old rows' rule), and the Ctrl+Z/Ctrl+Y bullet. Everything touched is inside the arc's paths: `Domain/Journal`, `UseCases/Journal`, `Infrastructure/Journal`, `TsumugiDbContext`, `ChangeLogEntryConfiguration`, one new migration plus the model snapshot, ADR 0022, and the journal tests. No seed change: the arc adds no user-authored content. No other migration landed on main since the branch; the branch is rebased on `b0e8bff` (#52, the manuscript hardening, which touches none of these files). ## Verification - **Suites** (on `b0e8bff`, modules rebuilt): Integration 530/530, BlazorAdapter 415/415, Domain 486/486, UseCases 236/236. The journal-related filter (HistoryAndRevert, FieldJournal, Journal, every migration test, ImageField, AppSettings, JournalCompatibility — 97 tests) passed 10 runs in a row; the full Integration suite passed 3 runs; the adapter's History, ChapterPage and ManuscriptEffects tests (70) pass. The Kagaku.Flow suites were not run: nothing here touches them. - **New tests** (21 — 17 Integration, 4 Domain): - `HistoryAndRevertTests`: both repro sequences — `Ctrl_Z_of_a_new_edit_brings_back_that_edit_and_never_the_split_the_edit_cleared` (Ctrl+Y, with a neighbour story whose own undone rename waits below) and `Ctrl_Z_of_the_edit_that_cleared_a_revert_leaves_History_s_Redo_that_edit_alone`; bug 2's two-snapshot case, `Revert_to_here_on_an_older_snapshot_leaves_Ctrl_Y_nothing_and_History_s_Redo_the_whole_revert` (×2: with and without a Ctrl+Z first, another story written between); `Revert_to_the_version_before_a_run_leaves_Ctrl_Y_nothing_to_replay_onto_the_story_without_it`; `A_new_edit_clears_a_run_revert_from_History_s_Redo_and_Redo_run_still_brings_the_run_back`; `A_forced_run_revert_of_the_newest_edit_leaves_Ctrl_Y_nothing_until_the_run_is_back` (×2: History's Redo (2,2), or Redo run then Ctrl+Y (1,1)). The #44 round trip stays pinned by the existing `A_version_saved_over_an_undone_edit_leaves_Ctrl_Y_the_edits_made_after_it`. - `FieldJournalTests.A_new_operation_clears_the_row_the_forced_revert_discarded_and_redo_brings_back_only_what_Ctrl_Z_took` (×2); the PR #25 rule stays pinned by the existing `Once_Ctrl_Z_has_made_the_field_change_whole_Ctrl_Y_re_applies_all_of_it_and_the_run_stays_reverted`. - `JournalTests.A_new_operation_clears_the_storys_waiting_rows_in_its_own_save_synchronous_or_not_and_a_failed_save_clears_nothing`. - `JournalUndoStateMigrationTests` (new; the world written through the use cases, walked down to `SegmentTextMarkup` and up): the column comes in empty and a walk down gives the old build its `ChangeLog` byte for byte; Up adds a column and an index without rebuilding a table; a pre-upgrade row with nothing waiting below it redoes by position and a new operation leaves it alone, while a neighbour story's waiting row below it stays the neighbour's; a version saved over a pre-upgrade undone row leaves Ctrl+Y the edits made after it; a pre-upgrade row above one a new operation clears is cleared with it (×2: Ctrl+Y and History's Redo give (1,1), the old paragraph stays gone, both redos then refuse). The stored numbers are read raw (1 Rewound, 3 Cleared). - `ChangeLogEntryTests` (Domain, 4): a new row waits for nothing; an undone row is rewound unless cut out; a redone row waits for nothing again; the three states keep the numbers the journal stores (1, 2, 3). - **Mutation probes** — every one compiled and restored its file on every exit (a trap), and the file was touched: - The implementation's 27 probes were each killed by a named test. - This round's 17 were all killed: - Clearing (`TsumugiDbContext.Waiting`): HEAD's rule restored, old rows never cleared → the FK repro Theory (both cases); `>=` bottom → `>` → 8 tests incl. the split test; no bottom (every undone row cleared) → the nothing-waiting-below migration test, the version-over migration test and `CategoryFieldsMigrationTests`' byte-for-byte walk; `IsUndone` dropped (active rows marked) → the FK repro Theory's own-row assertion; the bound's story filter dropped → the nothing-waiting-below test's neighbour; the outer story filter dropped → the split test's neighbour; the bound only Rewound → the run-revert and FieldJournal History tests; only CutOut → 6 tests; the sync clear removed → the JournalTests sync save; the async clear removed → 8 tests. - `RedoTyping`: a pre-upgrade row below the snapshot blocking Ctrl+Y as a rewound one does (the review's R4) → the new version-over migration test; the below-snapshot check disabled → 5 tests incl. the new forced-revert Theory. - `UndoState`: `Rewound = 1` renumbered to 4 (the review's E1) → the Domain numbers test and 3 migration tests. - `MarkUndone`'s default flipped to CutOut → the Domain test and 10 Integration tests. - `EfUndoStore`: the cut-out comparison inverted → 10 tests; every undo rewound → the PR #25 FieldJournal test; every undo cut out → 9 tests. - Equivalent and left alone (both found by the review): Down's `DropIndex` (SQLite's `DropColumn` rebuilds the table without the index anyway) and `HasConversion<int>()` (EF maps enums to int by default; kept for the file's idiom). A `!= Cleared` guard in the clearing query was an equivalent too (a cleared row set to cleared again) and is gone. - **Query plan**: the clearing `UPDATE`'s `WHERE` read through `EXPLAIN QUERY PLAN` on SQLite: `SEARCH c USING INDEX IX_ChangeLog_StoryId_Id (StoryId=? AND Id>?)`, bound by `SCALAR SUBQUERY` → `SEARCH c0 USING COVERING INDEX IX_ChangeLog_StoryId_UndoState (StoryId=? AND UndoState=?)`. - **Independent review**: two reviewers read the change; one also ran 36 compiling probes (31 killed, 2 equivalents, 2 survivors) and three clean full-suite reruns. What they found, and what was done: - **Major — a pre-upgrade undone row above a cleared one threw.** Before the upgrade: a chapter, a paragraph in it, the paragraph undone. After it: Ctrl+Z the chapter, a new paragraph elsewhere, its Ctrl+Z. The old paragraph's row (no state) was left waiting while the chapter's row was cleared, so Ctrl+Y and History's Redo both threw `SQLite Error 19: FOREIGN KEY constraint failed`, and kept throwing after every later "new edit, Ctrl+Z". Fixed: clearing takes every undone row from the story's lowest rewound or cut-out row up, so an old row above a cleared one goes with it. Pinned by the FK repro Theory; HEAD's rule restored fails it. - **Major — survivor R4.** Nothing pinned that a pre-upgrade row below the newest snapshot is stepped over like a cut-out one. Pinned by `A_version_saved_over_a_row_undone_before_the_upgrade_leaves_Ctrl_Y_the_edits_made_after_it`; R4 now fails it. - **Minor — survivor E1.** The stored integers were not pinned. Now a Domain test pins 1, 2, 3 and the migration tests read the raw column. - **Minor — old undone rows are never cleared, so bug 1 stays live for them, and the stated reason ("nothing guesses") did not hold for clearing.** Partly taken: rows above a cleared one are now cleared (above). Clearing *every* pre-upgrade undone row at the next operation breaks `CategoryFieldsMigrationTests.A_walk_down_and_up_keeps_every_bag_byte_for_byte…`, which dumps the whole `ChangeLog` across a walk that drops the column by design; that file is outside this arc, so it is a follow-up. ADR 0022, `UndoState` and the migration summary now give the real rule: rows undone before the change keep the old build's behaviour. - **Minor — the ADR said what, not why.** The Redo bullet now says position alone cannot carry the states (the two histories leave the same rows undone in the same order) and why a state per row rather than an "undone by" reference (read in strict undo order, it would refuse Ctrl+Y of a forced revert's operation once Ctrl+Z has made it whole — PR #25). - **Nit — "undone before 2026-09-26".** Now "undone before the journal recorded its state (the `JournalUndoState` migration)": the upgrade, not the calendar. - **Nit — Revert run still promised redoable discarded edits.** Qualified: redoable until a new operation clears them; while the run's rows wait below its snapshot, by History's Redo, or by Ctrl+Y once Redo run has brought the run back. Pinned by the new forced-revert Theory. - **Nit — `MarkUndone`'s default was the permissive CutOut.** Now `MarkUndone(bool cutOut = false)`: an undo that does not say is marked Rewound, where Ctrl+Y refuses. Making it required would break the adapter's `FakeUndoStore`, outside this arc (follow-up); the fake now models Ctrl+Z as rewound. - **Nit — the commit said the `EfUndoStore` path was closed.** Reworded: bug 2's route to it is closed, but a cut-out discarded edit over an entity a reverted run created still reaches it (follow-up). **Follow-ups** (outside this arc's paths): - Clear every pre-upgrade undone row at the next operation: `CategoryFieldsMigrationTests`' walk should compare its `ChangeLog` dump without `UndoState`, which the walk drops by design. - Make `MarkUndone`'s `cutOut` required once the adapter's `FakeUndoStore` passes it. - `EfUndoStore` marks a replayed update active when its entity is missing (reachable through a cut-out discarded edit over an entity a reverted run created). - History lists a cleared operation as plain "undone" (`HistoryOperation.IsUndone`); showing it as cleared is a UI choice for later. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(journal): redo that stays cleared — a new operation clears its story's waiting rows for good, Ctrl+Y replays nothing past a revert to an older version, and the journal records how each row was undone
Some checks failed
CI / test (pull_request) Has been cancelled
CI / editor-browsers (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
aeb43f1bb7
Two redo bugs only the journal itself could tell apart. Redo was worked out
from position alone, and "R undone, then S written, then S undone" leaves the
same IsUndone pattern as "R and S written, S undone, R undone":

- A new operation hid the redo set only while it stood: split, Ctrl+Z, type,
  Ctrl+Z, Ctrl+Y, Ctrl+Y re-applied the split; History's Redo after Revert to
  here, a new edit and its Ctrl+Z brought the reverted operations back.
- Ctrl+Y looked only above the newest snapshot: after Revert to here on the
  older of two versions it replayed the edit made after the newer one onto the
  reverted story, the operations between them still undone.

ChangeLog.UndoState (migration JournalUndoState, nullable, with an index on
(StoryId, UndoState)) records what an undone row waits for:

- Rewound — undone with no active row left above it (Ctrl+Z, Revert to here, a
  run revert with nothing after it); CutOut — undone from under a later active
  row (a run revert, a run redo's forced discard). EfUndoStore marks each row
  against its story's newest row still active; an undo that does not say is
  marked Rewound, the side where Ctrl+Y refuses.
- Cleared — every new operation marks, per story, each undone row from the
  lowest rewound or cut-out one up cleared, in its own transaction
  (TsumugiDbContext, both save paths: one seek on (StoryId, UndoState), one
  range of (StoryId, Id)). Ctrl+Y and History's Redo skip cleared rows;
  RedoRun stays run-scoped and still brings a cleared run back under its
  conflict check.
- RedoTyping takes the newest active row over the whole story (a MAX on the
  new LatestActivePositionAsync) and answers "nothing to redo" while a rewound
  row waits below the newest snapshot; a cut-out row there is stepped over, so
  a forced run revert's field change still comes back by Ctrl+Y once Ctrl+Z
  made it whole (ADR 0022's PR #25 rule).
- Rows undone before the migration carry no state — nothing is backfilled —
  and keep the old build's behaviour: they redo by position, and a new
  operation clears one only with a row undone since below it, whose redo would
  have to come first (replayed alone, an old paragraph would land in a chapter
  gone for good).

ADR 0022 amended (Status, the journal's columns, Revert run's discarded edits,
the Redo bullet with why a state per row, the Ctrl+Z/Ctrl+Y bullet). The
optional EfUndoStore fix (a replay onto a missing entity still marked active)
is left: bug 2's route to it is closed, but a cut-out discarded edit over an
entity a reverted run created still reaches it (ADR 0022's PR #25 behaviour).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bjoern referenced this pull request from a commit 2026-09-26 16:55:11 +02:00
docs: the redo set's PR is #56
All checks were successful
CI / build (pull_request) Successful in 41s
CI / editor-browsers (pull_request) Successful in 55s
CI / test (pull_request) Successful in 1m34s
11fd3496a3
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: 11fd349 · Generated: 2026-09-26 14:58:23 UTC · Revision: #1

Summary

Summary
Generated on: 09/26/2026 - 14:58:23
Coverage date: 09/26/2026 - 14:57:52 - 09/26/2026 - 14:58:18
Parser: MultiReport (6x Cobertura)
Assemblies: 9
Classes: 495
Files: 449
Line coverage: 98.1% (15688 of 15984)
Covered lines: 15688
Uncovered lines: 296
Coverable lines: 15984
Total lines: 33982
Branch coverage: 94.1% (5951 of 6318)
Covered branches: 5951
Total branches: 6318
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 - 96%
Name Line Branch
Tsumugi.BlazorAdapter 96% 90.9%
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% 93.5%
Tsumugi.BlazorAdapter.Documents.DocumentsLoaded 100%
Tsumugi.BlazorAdapter.Documents.DocumentsPage 100% 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% 80%
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 91.1% 84.9%
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.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.7%
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.3% 86.6%
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.5%
Name Line Branch
Tsumugi.UseCases 98.5% 95.7%
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 75% 57.1%
Tsumugi.UseCases.Journal.RedoTyping 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.ReorderSegments 100% 100%
Tsumugi.UseCases.Segments.ReshapeSegments 100% 100%
Tsumugi.UseCases.Segments.ReshapeWrite 100%
Tsumugi.UseCases.Segments.SegmentDto 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:** `11fd349` · **Generated:** 2026-09-26 14:58:23 UTC · **Revision:** #1 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/26/2026 - 14:58:23 | | Coverage date: | 09/26/2026 - 14:57:52 - 09/26/2026 - 14:58:18 | | Parser: | MultiReport (6x Cobertura) | | Assemblies: | 9 | | Classes: | 495 | | Files: | 449 | | **Line coverage:** | 98.1% (15688 of 15984) | | Covered lines: | 15688 | | Uncovered lines: | 296 | | Coverable lines: | 15984 | | Total lines: | 33982 | | **Branch coverage:** | 94.1% (5951 of 6318) | | Covered branches: | 5951 | | Total branches: | 6318 | | **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 - 96%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**96%**|**90.9%**| |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%|93.5%| |Tsumugi.BlazorAdapter.Documents.DocumentsLoaded|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsPage|100%|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%|80%| |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|91.1%|84.9%| |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.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.7%**| |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.3%|86.6%| |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.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**98.5%**|**95.7%**| |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|75%|57.1%| |Tsumugi.UseCases.Journal.RedoTyping|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.ReorderSegments|100%|100%| |Tsumugi.UseCases.Segments.ReshapeSegments|100%|100%| |Tsumugi.UseCases.Segments.ReshapeWrite|100%|| |Tsumugi.UseCases.Segments.SegmentDto|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 my~ A tri-state journal column that finally makes the redo set say what it waits for, instead of trying to squeeze two different histories out of one position! "R undone, S written, S undone" versus "R and S written, S undone, R undone" — you're right, no positional rule can ever tell those apart. State per row is the only honest answer, and watching EfUndoStore compute newestLeft per story to split Rewound from CutOut made me genuinely giddy~ ♡

Verdict: ✅ Looks good to me~

I came to break this, you know. That's my job~ And I couldn't.

✅ What I liked~

  • The design is right. MarkUndone(bool cutOut = false) defaults to Rewound — the refusing side — so an undo that doesn't say which it was can never let Ctrl+Y replay something dangerous. A safe default on a state machine is the kind of thing I want to frame and hang on a wall. ♪
  • Clearing rides the operation's own transaction on both save paths, after the journal rows land, as a single ExecuteUpdate per story bounded by a scalar subquery — and the failed-save test proves a rejected save clears nothing. I reproduced your query plan claim independently on SQLite: SEARCH IX_ChangeLog_StoryId_Id (StoryId=? AND Id>?) bound by SCALAR SUBQUERY → COVERING INDEX IX_ChangeLog_StoryId_UndoState. Two index probes when nothing waits. Exactly as advertised~
  • The empty-Min edge (>= NULL matching nothing, no rows cleared) is correct, and the FK-crash fix from the round-0 review — an old stateless row cleared with a newer waiting row below it — is exactly the right shape: the old row's redo could never come anyway without landing in a deleted chapter.
  • RedoRevert.Target dropping IsUndone is sound, not sly: every undone row above the newest active row must have UndoState ∈ {Rewound, CutOut, Cleared} (stateful) or null (pre-upgrade, documented old-build behavior); a null active row above newestActive = 0 is unreachable. I traced every arm.
  • RedoRun untouched is correct: run rows stay run-scoped, cleared or not, under its own conflict check — pinned by A_new_edit_clears_a_run_revert….

My probes — six compiled mutants, six killed, each restored pristine (git diff --quiet after every one):

  1. >= → > in the clearing bound → 6 red in the journal filter.
  2. Below-snapshot rule inverted (block on != CutOut) → exactly 1 red on exactly the R4 migration pin.
  3. Bound subquery's story filter dropped → the neighbour-story migration pin went red. Cross-story leak caught~
  4. !IsUndone dropped from LatestActivePositionAsync → 13 red.
  5. Every undo rewound in EfUndoStore → exactly 1 red, the PR #25 FieldJournal pin. Matches your table.
  6. MarkUndone default flipped to cutOut: true → 1 red in Domain (see below).

Suites, digit-exact vs your body: Integration 530/530, BlazorAdapter 415/415, Domain 486/486, UseCases 236/236, Release build 0w/0e. Tree pristine, submodules untouched.

💡 Little ideas (non-blocking)~

  1. PR body, probe table — the MarkUndone default-flip row claims "the Domain test and 10 Integration tests". It doesn't reproduce: with a freshly verified mutated build, the full Integration suite stays 530/530 (every Integration-path call site — EfUndoStore — passes cutOut: explicitly; only FakeUndoStore and the Domain test call it bare, and the fake lives in the adapter suite). The "10 Integration tests" belong to the adjacent comparison-inverted mutant in your own list. The default is pinned (the Domain test went red the moment I flipped it — I made sure the build was fresh ♡), so this is purely a record-keeping nit in the body, nothing more.
  2. The four follow-ups you list (pre-upgrade rows cleared at the next operation vs CategoryFieldsMigrationTests' byte-for-byte walk; cutOut required once FakeUndoStore passes it; the replayed-update-active-when-entity-missing path; History's UI showing cleared as plain "undone") — all correctly outside this arc, all real, all safely deferred. I checked each against the code and agree with the scoping.

Fufu~ the ADR amendment even explains why position cannot carry the states and why a state rather than an undone-by reference (the PR #25 constraint). That's the documentation caring as much as I do~ ♡


Automated review by Jibril · 2026-09-26
CI/CD: absent for head SHA (PR minutes old, no bot comment) · Local checks: Release 0w/0e; Integration 530/530, BlazorAdapter 415/415, Domain 486/486, UseCases 236/236; 6 mutation probes killed

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ A tri-state journal column that finally makes the redo set *say what it waits for*, instead of trying to squeeze two different histories out of one position! "R undone, S written, S undone" versus "R and S written, S undone, R undone" — you're right, no positional rule can ever tell those apart. State per row is the only honest answer, and watching `EfUndoStore` compute `newestLeft` per story to split Rewound from CutOut made me genuinely giddy~ ♡ ### Verdict: ✅ Looks good to me~ I came to break this, you know. That's my job~ And I couldn't. #### ✅ What I liked~ - **The design is right.** `MarkUndone(bool cutOut = false)` defaults to Rewound — the *refusing* side — so an undo that doesn't say which it was can never let Ctrl+Y replay something dangerous. A safe default on a state machine is the kind of thing I want to frame and hang on a wall. ♪ - **Clearing rides the operation's own transaction** on both save paths, after the journal rows land, as a single `ExecuteUpdate` per story bounded by a scalar subquery — and the failed-save test proves a rejected save clears *nothing*. I reproduced your query plan claim independently on SQLite: `SEARCH IX_ChangeLog_StoryId_Id (StoryId=? AND Id>?)` bound by `SCALAR SUBQUERY → COVERING INDEX IX_ChangeLog_StoryId_UndoState`. Two index probes when nothing waits. Exactly as advertised~ - **The empty-`Min` edge** (`>= NULL` matching nothing, no rows cleared) is correct, and the FK-crash fix from the round-0 review — an old stateless row cleared *with* a newer waiting row below it — is exactly the right shape: the old row's redo could never come anyway without landing in a deleted chapter. - **`RedoRevert.Target` dropping `IsUndone` is sound**, not sly: every undone row above the newest active row must have `UndoState ∈ {Rewound, CutOut, Cleared}` (stateful) or `null` (pre-upgrade, documented old-build behavior); a `null` *active* row above `newestActive = 0` is unreachable. I traced every arm. - **`RedoRun` untouched is correct**: run rows stay run-scoped, cleared or not, under its own conflict check — pinned by `A_new_edit_clears_a_run_revert…`. **My probes — six compiled mutants, six killed, each restored pristine** (`git diff --quiet` after every one): 1. `>=` → `>` in the clearing bound → 6 red in the journal filter. 2. Below-snapshot rule inverted (block on `!= CutOut`) → exactly 1 red on exactly the R4 migration pin. 3. Bound subquery's story filter dropped → the neighbour-story migration pin went red. Cross-story leak caught~ 4. `!IsUndone` dropped from `LatestActivePositionAsync` → 13 red. 5. Every undo rewound in `EfUndoStore` → exactly 1 red, the PR #25 FieldJournal pin. Matches your table. 6. `MarkUndone` default flipped to `cutOut: true` → 1 red in **Domain** (see below). Suites, digit-exact vs your body: Integration **530/530**, BlazorAdapter **415/415**, Domain **486/486**, UseCases **236/236**, Release build 0w/0e. Tree pristine, submodules untouched. #### 💡 Little ideas (non-blocking)~ 1. **PR body, probe table** — the `MarkUndone` default-flip row claims "the Domain test **and 10 Integration tests**". It doesn't reproduce: with a freshly verified mutated build, the full Integration suite stays 530/530 (every Integration-path call site — `EfUndoStore` — passes `cutOut:` explicitly; only `FakeUndoStore` and the Domain test call it bare, and the fake lives in the adapter suite). The "10 Integration tests" belong to the adjacent *comparison-inverted* mutant in your own list. The default **is** pinned (the Domain test went red the moment I flipped it — I made sure the build was fresh ♡), so this is purely a record-keeping nit in the body, nothing more. 2. The four follow-ups you list (pre-upgrade rows cleared at the next operation vs `CategoryFieldsMigrationTests`' byte-for-byte walk; `cutOut` required once `FakeUndoStore` passes it; the replayed-update-active-when-entity-missing path; History's UI showing cleared as plain "undone") — all correctly outside this arc, all real, all safely deferred. I checked each against the code and agree with the scoping. Fufu~ the ADR amendment even explains *why position cannot carry the states* and *why a state rather than an undone-by reference* (the PR #25 constraint). That's the documentation caring as much as I do~ ♡ --- *Automated review by Jibril · 2026-09-26* *CI/CD: absent for head SHA (PR minutes old, no bot comment) · Local checks: Release 0w/0e; Integration 530/530, BlazorAdapter 415/415, Domain 486/486, UseCases 236/236; 6 mutation probes killed*
Author
Member

Correction to the Verification section (Jibril is right): the probe line "MarkUndone's default flipped to CutOut → the Domain test and 10 Integration tests" is wrong. With that mutant Integration stays 530/530, because EfUndoStore.cs:58, the only production caller, always passes cutOut: explicitly. Only ChangeLogEntryTests (Domain) kills it; the default is pinned there. The "10 tests" belong to the next line, "EfUndoStore: the cut-out comparison inverted". No code change.

**Correction to the Verification section** (Jibril is right): the probe line "`MarkUndone`'s default flipped to CutOut → the Domain test and 10 Integration tests" is wrong. With that mutant Integration stays 530/530, because `EfUndoStore.cs:58`, the only production caller, always passes `cutOut:` explicitly. Only `ChangeLogEntryTests` (Domain) kills it; the default is pinned there. The "10 tests" belong to the next line, "`EfUndoStore`: the cut-out comparison inverted". No code change.
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my. A whole epistemology of undone rows — the journal doesn't just remember that a row was undone, it remembers what each row waits for. Rewound, CutOut, Cleared — three little states that dissolve the exact ambiguity that sank #44's positional rule ("R undone, S written, S undone" vs "R and S written, S undone, R undone" — same rows, same order, different histories). I read all 1,183 lines, then chased every reader and writer of UndoState through the whole codebase. This is the tidiest piece of journal surgery I've reviewed in weeks~ ♪

Verdict: ✅ Looks good to me~

The core trace, verified by hand:

  • EfUndoStore.NewestActiveLeftAsync — the Rewound/CutOut split is computed per story against the newest row left active, excluding exactly the ids being undone (so a story undoing only its own top rows isn't misclassified by a neighbour's — or another story's — active rows). Correct.
  • TsumugiDbContext.Waiting() — the clearing runs after base.SaveChanges inside the operation's own transaction (before Commit), so a failed save rolls the clear back with the operation: atomic by construction. The bound (lowest Rewound/CutOut row) intentionally swallows old pre-upgrade rows sitting above a cleared one — that's the FK-repro fix from the independent review, and A_row_undone_before_the_upgrade_above_one_a_new_operation_clears_is_cleared_with_it pins it. The >= bottom is load-bearing: the waiting row itself is cleared (probe-verified).
  • RedoTyping.TargetAsync — the whole-story LatestActivePositionAsync MAX is the real fix for bug 2: the floor is now min(snapshot floor, newest active), the below-floor Rewound veto refuses Ctrl+Y after Revert to here on an older snapshot, and CutOut rows below the floor are stepped over. The Math.Min isn't decorative — with only floor, rows between an older snapshot and the newest active row would fall out of ListAfterAsync's window entirely.
  • RedoRevert.Target — != Cleared on the whole-journal scan; GetStoryHistory.CanRedo rides the same Target, so the History tab and the button agree by construction.
  • RedoRun stays state-blind deliberately — run-scoped, still brings cleared run rows back under its conflict check. Consistent with the ADR amendment.
  • UndoTyping/RevertToSnapshot only ever pass !IsUndone rows to UndoAsync, and RevertRun/RedoRun conflicts are active by definition — so no path re-stamps a Cleared row back to waiting. The stamp discipline holds everywhere. ✧

Mutation probes (mine, 6 total, each killed on exactly its own pin, each restored pristine):

  1. Math.Min(floor, newestActive) → floor: Revert_to_here_on_an_older_snapshot (2/2) and Revert_to_the_version_before_a_run both red. Restored → green.
  2. Below-floor Rewound veto deleted: Revert_to_here_on_an_older_snapshot (2/2) + A_forced_run_revert_of_the_newest_edit (2/2) red.
  3. RedoRevert != Cleared filter dropped: Ctrl_Z_of_the_edit_that_cleared_a_revert and A_new_edit_clears_a_run_revert red.
  4. Waiting() bound >= → >: Ctrl_Z_of_a_new_edit_brings_back_that_edit_and_never_the_split_the_edit_cleared red.
  5. cutOut: row.Id < newestLeft inverted to >: red on Revert_to_here_on_an_older_snapshot, A_row_undone_before_the_upgrade_with_nothing_waiting_below_it, A_forced_run_revert_of_the_newest_edit (my first filter aimed at non-observing tests and survived — re-aimed, killed; a good reminder that a probe is only as good as its pin ♡).
  6. Down-migration DropIndex + HasConversion<int> left alone — your equivalence analysis matches mine (SQLite's DropColumn rebuilds the table without the index; the conversion is idiom).

Local, at head 11fd349 (submodules Kagaku.UI 6af0979 + OpenRouter.Net b002f5e): Release build 0 warnings / 0 errors; Domain 486/486, UseCases 236/236, Integration 530/530, BlazorAdapter 415/415 — all four digit-exact against the PR body.

One honest observation (non-blocking, for the record): my first two full Integration runs each had a single failure in JournalUndoStateMigrationTests (run 1: A_version_saved_over… line 133; run 2: A_row_undone_before_the_upgrade… line 110, Assert.Null saw 3). Both green in isolation and on every rerun. I could not reproduce in 12 further full runs (10 with a step-by-step state dump of the exact scenario — every __EFMigrationsHistory row, the column's presence, and each journal row at every step, all coherent). I traced every write path in the diff: within a coherent database state the failing assert cannot read 3 (it requires the pre-upgrade row to carry Rewound, which the test's own earlier asserts exclude), and every theory of the down→up dance silently skipping requires a dance the same test had already proven happened. So I make no root-cause claim — the evidence does not support one. The two red runs sat in a window where this sandbox was also building twice concurrently; the 12 green runs and the step-dumped trace say the PR's logic is sound. If it ever recurs in CI, the dump harness above is the tool that will catch it mid-act.

✅ What I liked~

  • The state-per-row vs. undone-by-reference argument in the ADR amendment is genuinely good design writing — the PR #25 counterexample (a reference read in undo order would refuse Ctrl+Y of a forced revert's operation once Ctrl+Z made it whole) is the kind of why that stops the next person from re-deriving the wrong design.
  • MarkUndone(bool cutOut = false) defaulting to Rewound, the refusing side — fail-closed. ♡
  • Migration hygiene: nullable column, no table rebuild, EXPLAIN QUERY PLAN cited for the clearing UPDATE, stored integers pinned by a Domain test so a renumber can never silently change what every stored row means.
  • The neighbour stories woven through the repro tests — every scoping seam (per-story clearing, per-story active bound) has a witness that would catch a cross-story leak.

Fufu~ the redo set stays cleared, Ctrl+Y stays in its lane, and the journal finally knows what it's waiting for. Ready to merge~


Automated review by Jibril · 2026-09-26 17:34 CEST
CI/CD: absent for head 11fd349 (no bot comment yet) · Local checks: 4 suites green digit-exact, 6 mutation probes killed, tree pristine post-probes

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my. A whole *epistemology of undone rows* — the journal doesn't just remember *that* a row was undone, it remembers **what each row waits for**. Rewound, CutOut, Cleared — three little states that dissolve the exact ambiguity that sank #44's positional rule ("R undone, S written, S undone" vs "R and S written, S undone, R undone" — same rows, same order, different histories). I read all 1,183 lines, then chased every reader and writer of `UndoState` through the whole codebase. This is the tidiest piece of journal surgery I've reviewed in weeks~ ♪ ### Verdict: ✅ Looks good to me~ **The core trace, verified by hand:** - **`EfUndoStore.NewestActiveLeftAsync`** — the Rewound/CutOut split is computed *per story* against the newest row left active, excluding exactly the ids being undone (so a story undoing only its own top rows isn't misclassified by a neighbour's — or another story's — active rows). Correct. - **`TsumugiDbContext.Waiting()`** — the clearing runs *after* `base.SaveChanges` inside the operation's own transaction (before `Commit`), so a failed save rolls the clear back with the operation: atomic by construction. The bound (lowest `Rewound`/`CutOut` row) intentionally swallows old pre-upgrade rows sitting above a cleared one — that's the FK-repro fix from the independent review, and `A_row_undone_before_the_upgrade_above_one_a_new_operation_clears_is_cleared_with_it` pins it. The `>=` bottom is load-bearing: the waiting row itself is cleared (probe-verified). - **`RedoTyping.TargetAsync`** — the whole-story `LatestActivePositionAsync` MAX is the real fix for bug 2: the floor is now `min(snapshot floor, newest active)`, the below-floor `Rewound` veto refuses Ctrl+Y after *Revert to here* on an older snapshot, and CutOut rows below the floor are stepped over. The `Math.Min` isn't decorative — with only `floor`, rows between an older snapshot and the newest active row would fall out of `ListAfterAsync`'s window entirely. - **`RedoRevert.Target`** — `!= Cleared` on the whole-journal scan; `GetStoryHistory.CanRedo` rides the same `Target`, so the History tab and the button agree by construction. - **`RedoRun` stays state-blind deliberately** — run-scoped, still brings cleared run rows back under its conflict check. Consistent with the ADR amendment. - **`UndoTyping`/`RevertToSnapshot` only ever pass `!IsUndone` rows to `UndoAsync`**, and `RevertRun`/`RedoRun` conflicts are active by definition — so no path re-stamps a `Cleared` row back to waiting. The stamp discipline holds everywhere. ✧ **Mutation probes (mine, 6 total, each killed on exactly its own pin, each restored pristine):** 1. `Math.Min(floor, newestActive)` → `floor`: `Revert_to_here_on_an_older_snapshot` (2/2) and `Revert_to_the_version_before_a_run` both red. Restored → green. 2. Below-floor `Rewound` veto deleted: `Revert_to_here_on_an_older_snapshot` (2/2) + `A_forced_run_revert_of_the_newest_edit` (2/2) red. 3. `RedoRevert` `!= Cleared` filter dropped: `Ctrl_Z_of_the_edit_that_cleared_a_revert` and `A_new_edit_clears_a_run_revert` red. 4. `Waiting()` bound `>=` → `>`: `Ctrl_Z_of_a_new_edit_brings_back_that_edit_and_never_the_split_the_edit_cleared` red. 5. `cutOut: row.Id < newestLeft` inverted to `>`: red on `Revert_to_here_on_an_older_snapshot`, `A_row_undone_before_the_upgrade_with_nothing_waiting_below_it`, `A_forced_run_revert_of_the_newest_edit` (my first filter aimed at non-observing tests and survived — re-aimed, killed; a good reminder that a probe is only as good as its pin ♡). 6. Down-migration `DropIndex` + `HasConversion<int>` left alone — your equivalence analysis matches mine (SQLite's `DropColumn` rebuilds the table without the index; the conversion is idiom). **Local, at head 11fd349 (submodules Kagaku.UI 6af0979 + OpenRouter.Net b002f5e):** Release build 0 warnings / 0 errors; Domain 486/486, UseCases 236/236, Integration 530/530, BlazorAdapter 415/415 — all four digit-exact against the PR body. **One honest observation (non-blocking, for the record):** my first two full Integration runs each had a single failure in `JournalUndoStateMigrationTests` (run 1: `A_version_saved_over…` line 133; run 2: `A_row_undone_before_the_upgrade…` line 110, `Assert.Null` saw `3`). Both green in isolation and on every rerun. I could not reproduce in **12 further full runs** (10 with a step-by-step state dump of the exact scenario — every `__EFMigrationsHistory` row, the column's presence, and each journal row at every step, all coherent). I traced every write path in the diff: within a coherent database state the failing assert *cannot* read `3` (it requires the pre-upgrade row to carry `Rewound`, which the test's own earlier asserts exclude), and every theory of the down→up dance silently skipping requires a dance the same test had already proven happened. So I make no root-cause claim — the evidence does not support one. The two red runs sat in a window where this sandbox was also building twice concurrently; the 12 green runs and the step-dumped trace say the PR's logic is sound. If it ever recurs in CI, the dump harness above is the tool that will catch it mid-act. #### ✅ What I liked~ - The **state-per-row vs. undone-by-reference** argument in the ADR amendment is genuinely good design writing — the PR #25 counterexample (a reference read in undo order would refuse Ctrl+Y of a forced revert's operation once Ctrl+Z made it whole) is the kind of *why* that stops the next person from re-deriving the wrong design. - `MarkUndone(bool cutOut = false)` defaulting to **Rewound, the refusing side** — fail-closed. ♡ - Migration hygiene: nullable column, no table rebuild, `EXPLAIN QUERY PLAN` cited for the clearing UPDATE, stored integers pinned by a Domain test so a renumber can never silently change what every stored row means. - The neighbour stories woven through the repro tests — every scoping seam (per-story clearing, per-story active bound) has a witness that would catch a cross-story leak. Fufu~ the redo set stays cleared, Ctrl+Y stays in its lane, and the journal finally knows what it's waiting for. Ready to merge~ --- *Automated review by Jibril · 2026-09-26 17:34 CEST* *CI/CD: absent for head 11fd349 (no bot comment yet) · Local checks: 4 suites green digit-exact, 6 mutation probes killed, tree pristine post-probes*
bjoern merged commit 817465f9c6 into main 2026-09-26 17:39:55 +02:00
bjoern deleted branch fix/redo-discarded-rows 2026-09-26 17:39:55 +02:00
Author
Member

Follow-up: the EfUndoStore one is fixed for Ctrl+Y in #58

This is the third follow-up listed above, Jibril's own suggestion: "EfUndoStore marks a replayed update active when its entity is missing". The plan was to add it here as one more commit, but this PR merged first, so it is in #58, based on the merge 817465f.

  • What #58 does: Ctrl+Y (RedoTyping.TargetAsync, which GetUndoStatus also reads) refuses an operation whose replayed rows edit or delete an entity the journal took away, meaning its create is undone or its delete still stands. It checks this with a new IChangeJournal.AnyRemovedAsync. The button and the key both answer "Nothing to redo." instead of marking the edit redone over nothing.
  • Routes: it covers the route named here (a forced run revert with an edit elsewhere, then Ctrl+Z, then Ctrl+Y) and a second one (Redo run deletes a paragraph again while an edit of it waits).
  • ADR 0022: it gets a Status line of its own, "Amended 2026-09-26 (PR #58, a redo onto nothing)". This PR's line is left as merged.
  • Tests and probes: 5 new cases: 4 across 3 tests in HistoryAndRevertTests, plus 1 in JournalUndoStateMigrationTests for rows with no state from before the upgrade. There were 10 compiling mutations, and every one was killed, including the two that survived Jibril's first pass over the commit.
  • Suites: Integration 535/535, BlazorAdapter 415/415, UseCases 236/236, Domain 486/486.
  • Still open: History's Redo and Redo run can still mark a replay onto a missing entity as done. The details are in #58's follow-ups.
**Follow-up: the `EfUndoStore` one is fixed for Ctrl+Y in #58** This is the third follow-up listed above, Jibril's own suggestion: "`EfUndoStore` marks a replayed update active when its entity is missing". The plan was to add it here as one more commit, but this PR merged first, so it is in #58, based on the merge 817465f. - **What #58 does:** Ctrl+Y (`RedoTyping.TargetAsync`, which `GetUndoStatus` also reads) refuses an operation whose replayed rows edit or delete an entity the journal took away, meaning its create is undone or its delete still stands. It checks this with a new `IChangeJournal.AnyRemovedAsync`. The button and the key both answer "Nothing to redo." instead of marking the edit redone over nothing. - **Routes:** it covers the route named here (a forced run revert with an edit elsewhere, then Ctrl+Z, then Ctrl+Y) and a second one (Redo run deletes a paragraph again while an edit of it waits). - **ADR 0022:** it gets a Status line of its own, "Amended 2026-09-26 (PR #58, a redo onto nothing)". This PR's line is left as merged. - **Tests and probes:** 5 new cases: 4 across 3 tests in `HistoryAndRevertTests`, plus 1 in `JournalUndoStateMigrationTests` for rows with no state from before the upgrade. There were 10 compiling mutations, and every one was killed, including the two that survived Jibril's first pass over the commit. - **Suites:** Integration 535/535, BlazorAdapter 415/415, UseCases 236/236, Domain 486/486. - **Still open:** History's Redo and Redo run can still mark a replay onto a missing entity as done. The details are in #58's follow-ups.
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!56
No description provided.