feat: Phase 1 · 6/7 — the story workspace #10

Merged
bjoern merged 3 commits from feat/p1-workspace into main 2026-09-22 21:41:24 +02:00
Member

Slice 6 of Phase 1: the manuscript editor behind /stories/{id} (docs/stories/story-workspace.md; ADRs 0010, 0012, 0022). Three commits: b464ff4 adds the RedoRevert use case (History's redo after a revert-to-here), 5a14062 is the workspace, f2f6fee is review round 1 (see the comment below).

What's in

  • The column (ManuscriptColumn, SegmentRow): the current chapter's paragraphs as one column of borderless textareas (the design system's TextArea, so typing never round-trips through the render tree), scene breaks as * * *, a provenance gutter (you / a run / a run then edited), a ghost delete per row, "Write the first paragraph" for an empty chapter and "Add a paragraph" at the tail. The paragraph the writer is in is an anchor for a toolbar insert only while it is in the chapter on screen; a chapter switch clears it.
  • Auto-save: each row keeps its own pending pause (a Debounce on the injected clock) and saves it as one journaled edited a paragraph; leaving the row saves at once. A row adopts the stored copy only while idle — our own save's echo, a run's write, another session's edit — keyed on the stamp and the text, and a trimmed echo (Segment trims trailing whitespace) never rewrites the element under a writer who just typed a space and paused. Same rule for the chapter header and the story details, which use the domain's own Text.BlankToNull (now public) to recognise their echo.
  • Structural keys through one delegated keydown listener (ManuscriptColumn.razor.js): Enter splits at the caret (Shift+Enter stays a newline), Backspace at the very start merges into the previous paragraph; a held key does not repeat, an IME mid-word is left alone. Each call carries the textarea's text as typed, and the effect saves it before it cuts — a refused save stops the cut. The caret offset counts the text as typed while the store holds it trimmed, so the split clamps the offset to the trimmed length, and typed text that differs only by trailing whitespace counts as already saved. The caret lands on the new paragraph / the join through a tokened CaretRequest the column applies once. Backspace on the first paragraph or behind a scene break is a silent no-op.
  • Chapters (ChapterBar, ChapterHeader): a Select switcher, "New chapter" (appended as Chapter N, rename it in the header), a menu with Move up / Move down / Merge into previous / Delete chapter (confirmed; edges disabled), the title and summary auto-saved from the header, a run-written summary saying so in the hint. The chapter on screen is pinned by id the moment a manuscript lands — the browser pass caught that "the first chapter" moved with a reorder and took the view with it.
  • The page (WorkspacePage, StoryDetails): breadcrumb, title, a SaveIndicator that shows Unsaved changes while any row, the header or the details holds a pending pause, Undo/Redo buttons with the next operation in their title, a Scene break button (after the focused paragraph), an InlineAlert for refused structural edits, the story's own title/description under a Disclosure. Ctrl+Z / Ctrl+Y are page hotkeys that flush every pending pause first, so they take back what was just typed rather than the pause before it. Not found / trashed / loading each get their own EmptyState. The page takes the scroll region and splits into two panes per ADR 0010.
  • History tab (HistoryPanel, HistoryEffects): the operations since the last version, the versions newest first (Run / Your edits / Version badges) with their operations (· N changes, Undone), "Save a version" behind a labelled modal, "Revert to here" behind a confirmation, and Redo (the new RedoRevert: every undone row above the newest active row, oldest first; StoryHistory.CanRedo drives the button). All three await the page's flush first, as Ctrl+Z does: a version holds what was just typed, and a revert takes a mid-pause edit back too. The tab re-queries on any journaled kind for the open story, documents included.
  • One Fluxor slice: WorkspaceState, one action record per file, WorkspaceReducers (every answer names its story; a late one for a story the workspace has left is dropped), ManuscriptEffects + HistoryEffects over the use cases. DomainChangesReceived reloads the manuscript on Story (own row) / Chapter / Segment and the History on those plus StorySnapshot / Document / DocumentCategory. A page never re-queries on its own: an announced change reloads, a silent one is not chased (test: Coming_back_to_the_same_story_reuses_the_loaded_slice…).
  • Story list: the title links to the workspace, the row menu gains Open, and creating a story lands in it (StoriesEffects navigates after StoryCreated).
  • Docs: the workspace story's boxes for what is built, the list's Open box, REQUIREMENTS §2's workspace and auto-save/undo/History boxes, AGENTS.md's status paragraph.

Deferred and why

  • Delete at the end of a paragraph joining the next: the story names Enter and Backspace; Delete-at-end is symmetrical but unspecified, and the merge use case only joins into the previous. A follow-up once it is wanted.
  • Revert run / Redo run (run-scoped with conflicts): the use cases exist since slice 3, the UI needs a run to point at — Phase 2, with the first run.
  • Drag reorder of chapters: the menu's Move up/down is the accessible baseline; drag needs the JS gesture path (ADR 0010's pointer rule) and is not in the story's AC.
  • The Run button and provenance → inspector jump: Phase 2 (graphs, runs).
  • Seed data and backups: their own slices (7 and later), as planned.
  • A scoped gate serialising effects over the circuit's DbContext (review round 1, 💡 1): app-wide rather than workspace-scoped, and nothing can interleave on SQLite today — its own PR if the provider changes.

Tests

project main this branch
Tsumugi.Domain.Tests 43 43
Tsumugi.UseCases.Tests 11 11
Tsumugi.BlazorAdapter.Tests 70 136
Tsumugi.Integration.Tests 101 104
GraphEditor (Vitest) 14 14 (untouched, not re-run)

New: WorkspaceReducersTests (12), ManuscriptEffectsTests (18), HistoryEffectsTests (3), WorkspacePageTests (28, incl. split/merge through the [JSInvokable] seam, the trimmed-echo guards for paragraph / chapter title / story title, Ctrl+Z flushing the header and the details, the pinned chapter under a reorder, the scene break after a chapter switch and after a paragraph vanished elsewhere, Enter after a trailing space, the History flush over the real store, and the cross-circuit case with the DomainChangedBridge rendered beside the page), HistoryPanelTests (6), AppChromeTests +1 (the story route behind the gate), HistoryAndRevertTests +2 (RedoRevert whole and around a reverted run in the middle), StoriesEffectsTests / StoriesPageTests updated (navigation, the Open link). In-memory doubles: a full FakeSegmentStore (boundary checks like the real one), FakeChangeJournal (assigns positions like the database), FakeSnapshotStore, FakeUndoStore, TestDomainChangedBus, FakeWorld, TestNavigationManager; AdapterTestContext registers them all with a FakeTimeProvider clock. Mutation-probed (disabling each fails exactly its tests): the three echo guards, the chapter pin, the on-screen focus anchor, the offset clamp, the History flush.

Coverage (coverlet, merged across test projects, paths normalised)

BlazorAdapter/Workspace/**, BlazorAdapter/Stories/**, UseCases/Journal/{RedoRevert,GetStoryHistory,HistoryModels}.cs, Domain/Text.cs: 882 / 889 lines, 479 / 496 branches. Not covered, and why:

  • HistoryModels.cs 7, 9, 10, 21 — positional record members the compiler generates.
  • ManuscriptColumn.razor 143–157 — DisposeAsync when the module never loaded and when the circuit is already gone (JSDisconnectedException).
  • ChapterBar.razor 46 — Index < 0 ? null, the pinned chapter absent from the list (the reducer re-pins on load, so transient).
  • SegmentRow.razor 103, ChapterHeader.razor 67/73, StoryDetails.razor 64/70 — the value ?? "" arms on ValueChanged; the design system never reports null.
  • WorkspacePage.razor 155, 161–163 — the ?. on the _column / _header / _details refs, null only before the first render.
  • ChapterHeader.razor 37 (5/6), WorkspaceState.cs 53 (5/6) — one combination each of an idle-and-synced early return / a null manuscript with a non-matching id.

Browser-verified (Release build, Chrome via Playwright, light and dark)

Sign in → New story lands in the workspace → "Write the first paragraph", type, Unsaved changes → Saved → Enter at the end makes a new focused paragraph → Enter mid-sentence splits at the caret → Backspace at the start merges with the caret on the join (offset 12 checked) → Scene break → Ctrl+Z removes it, Ctrl+Y brings it back → New chapter, rename and summary from the header, switch back → Save a version → edit → Since the last version grows → Revert to here (confirmed) takes the edit back → Redo returns it → a second tab's paragraph appears in the first while the first keeps its own typing, both converge → paragraph and scene-break deletes from the ghost buttons → Move down keeps the moved chapter on screen, Move up → Delete chapter (confirmed) lands on the remaining one → the list's title links to the story.

Round 1: type "One two. " and Enter → two paragraphs, no alert → focus a paragraph, New chapter, Scene break → the break is the new chapter's only segment, no alert → type mid-pause, Save a version → the typing is in the store when the version is taken → type mid-pause, Revert to here → that edit shows as Undone.

Follow-ups

  • Delete-at-end join, drag reorder, Revert run / Redo run UI, the effect gate (above).
  • Slice 7: the documents panel, the seed data; backups after.

🤖 Generated with Claude Code

Slice 6 of Phase 1: the manuscript editor behind `/stories/{id}` (`docs/stories/story-workspace.md`; ADRs 0010, 0012, 0022). Three commits: `b464ff4` adds the `RedoRevert` use case (History's redo after a revert-to-here), `5a14062` is the workspace, `f2f6fee` is review round 1 (see the comment below). ## What's in - **The column** (`ManuscriptColumn`, `SegmentRow`): the current chapter's paragraphs as one column of borderless textareas (the design system's `TextArea`, so typing never round-trips through the render tree), scene breaks as `* * *`, a provenance gutter (you / a run / a run then edited), a ghost delete per row, "Write the first paragraph" for an empty chapter and "Add a paragraph" at the tail. The paragraph the writer is in is an anchor for a toolbar insert only while it is in the chapter on screen; a chapter switch clears it. - **Auto-save**: each row keeps its own pending pause (a `Debounce` on the injected clock) and saves it as one journaled `edited a paragraph`; leaving the row saves at once. A row adopts the stored copy only while idle — our own save's echo, a run's write, another session's edit — keyed on the stamp *and* the text, and a trimmed echo (`Segment` trims trailing whitespace) never rewrites the element under a writer who just typed a space and paused. Same rule for the chapter header and the story details, which use the domain's own `Text.BlankToNull` (now public) to recognise their echo. - **Structural keys** through one delegated `keydown` listener (`ManuscriptColumn.razor.js`): **Enter** splits at the caret (Shift+Enter stays a newline), **Backspace** at the very start merges into the previous paragraph; a held key does not repeat, an IME mid-word is left alone. Each call carries the textarea's text as typed, and the effect saves it before it cuts — a refused save stops the cut. The caret offset counts the text as typed while the store holds it trimmed, so the split clamps the offset to the trimmed length, and typed text that differs only by trailing whitespace counts as already saved. The caret lands on the new paragraph / the join through a tokened `CaretRequest` the column applies once. Backspace on the first paragraph or behind a scene break is a silent no-op. - **Chapters** (`ChapterBar`, `ChapterHeader`): a `Select` switcher, "New chapter" (appended as `Chapter N`, rename it in the header), a menu with Move up / Move down / Merge into previous / Delete chapter (confirmed; edges disabled), the title and summary auto-saved from the header, a run-written summary saying so in the hint. The chapter on screen is **pinned by id the moment a manuscript lands** — the browser pass caught that "the first chapter" moved with a reorder and took the view with it. - **The page** (`WorkspacePage`, `StoryDetails`): breadcrumb, title, a `SaveIndicator` that shows *Unsaved changes* while any row, the header or the details holds a pending pause, Undo/Redo buttons with the next operation in their title, a Scene break button (after the focused paragraph), an `InlineAlert` for refused structural edits, the story's own title/description under a `Disclosure`. `Ctrl+Z` / `Ctrl+Y` are page hotkeys that flush every pending pause first, so they take back what was just typed rather than the pause before it. Not found / trashed / loading each get their own `EmptyState`. The page takes the scroll region and splits into two panes per ADR 0010. - **History tab** (`HistoryPanel`, `HistoryEffects`): the operations since the last version, the versions newest first (Run / Your edits / Version badges) with their operations (`· N changes`, *Undone*), "Save a version" behind a labelled modal, "Revert to here" behind a confirmation, and **Redo** (the new `RedoRevert`: every undone row above the newest active row, oldest first; `StoryHistory.CanRedo` drives the button). All three await the page's flush first, as Ctrl+Z does: a version holds what was just typed, and a revert takes a mid-pause edit back too. The tab re-queries on any journaled kind for the open story, documents included. - **One Fluxor slice**: `WorkspaceState`, one action record per file, `WorkspaceReducers` (every answer names its story; a late one for a story the workspace has left is dropped), `ManuscriptEffects` + `HistoryEffects` over the use cases. `DomainChangesReceived` reloads the manuscript on Story (own row) / Chapter / Segment and the History on those plus StorySnapshot / Document / DocumentCategory. A page never re-queries on its own: an announced change reloads, a silent one is not chased (test: `Coming_back_to_the_same_story_reuses_the_loaded_slice…`). - **Story list**: the title links to the workspace, the row menu gains *Open*, and creating a story lands in it (`StoriesEffects` navigates after `StoryCreated`). - Docs: the workspace story's boxes for what is built, the list's *Open* box, REQUIREMENTS §2's workspace and auto-save/undo/History boxes, AGENTS.md's status paragraph. ## Deferred and why - **Delete at the end of a paragraph** joining the next: the story names Enter and Backspace; Delete-at-end is symmetrical but unspecified, and the merge use case only joins *into the previous*. A follow-up once it is wanted. - **Revert run / Redo run** (run-scoped with conflicts): the use cases exist since slice 3, the UI needs a run to point at — Phase 2, with the first run. - **Drag reorder of chapters**: the menu's Move up/down is the accessible baseline; drag needs the JS gesture path (ADR 0010's pointer rule) and is not in the story's AC. - **The Run button and provenance → inspector jump**: Phase 2 (graphs, runs). - **Seed data** and **backups**: their own slices (7 and later), as planned. - **A scoped gate serialising effects over the circuit's `DbContext`** (review round 1, 💡 1): app-wide rather than workspace-scoped, and nothing can interleave on SQLite today — its own PR if the provider changes. ## Tests | project | main | this branch | |---|---|---| | Tsumugi.Domain.Tests | 43 | 43 | | Tsumugi.UseCases.Tests | 11 | 11 | | Tsumugi.BlazorAdapter.Tests | 70 | **136** | | Tsumugi.Integration.Tests | 101 | **104** | | GraphEditor (Vitest) | 14 | 14 (untouched, not re-run) | New: `WorkspaceReducersTests` (12), `ManuscriptEffectsTests` (18), `HistoryEffectsTests` (3), `WorkspacePageTests` (28, incl. split/merge through the `[JSInvokable]` seam, the trimmed-echo guards for paragraph / chapter title / story title, Ctrl+Z flushing the header and the details, the pinned chapter under a reorder, the scene break after a chapter switch and after a paragraph vanished elsewhere, Enter after a trailing space, the History flush over the real store, and the cross-circuit case with the `DomainChangedBridge` rendered beside the page), `HistoryPanelTests` (6), `AppChromeTests` +1 (the story route behind the gate), `HistoryAndRevertTests` +2 (RedoRevert whole and around a reverted run in the middle), `StoriesEffectsTests` / `StoriesPageTests` updated (navigation, the Open link). In-memory doubles: a full `FakeSegmentStore` (boundary checks like the real one), `FakeChangeJournal` (assigns positions like the database), `FakeSnapshotStore`, `FakeUndoStore`, `TestDomainChangedBus`, `FakeWorld`, `TestNavigationManager`; `AdapterTestContext` registers them all with a `FakeTimeProvider` clock. Mutation-probed (disabling each fails exactly its tests): the three echo guards, the chapter pin, the on-screen focus anchor, the offset clamp, the History flush. ## Coverage (coverlet, merged across test projects, paths normalised) `BlazorAdapter/Workspace/**`, `BlazorAdapter/Stories/**`, `UseCases/Journal/{RedoRevert,GetStoryHistory,HistoryModels}.cs`, `Domain/Text.cs`: **882 / 889 lines, 479 / 496 branches**. Not covered, and why: - `HistoryModels.cs` 7, 9, 10, 21 — positional record members the compiler generates. - `ManuscriptColumn.razor` 143–157 — `DisposeAsync` when the module never loaded and when the circuit is already gone (`JSDisconnectedException`). - `ChapterBar.razor` 46 — `Index < 0 ? null`, the pinned chapter absent from the list (the reducer re-pins on load, so transient). - `SegmentRow.razor` 103, `ChapterHeader.razor` 67/73, `StoryDetails.razor` 64/70 — the `value ?? ""` arms on `ValueChanged`; the design system never reports null. - `WorkspacePage.razor` 155, 161–163 — the `?.` on the `_column` / `_header` / `_details` refs, null only before the first render. - `ChapterHeader.razor` 37 (5/6), `WorkspaceState.cs` 53 (5/6) — one combination each of an idle-and-synced early return / a null manuscript with a non-matching id. ## Browser-verified (Release build, Chrome via Playwright, light and dark) Sign in → New story lands in the workspace → "Write the first paragraph", type, *Unsaved changes* → *Saved* → Enter at the end makes a new focused paragraph → Enter mid-sentence splits at the caret → Backspace at the start merges with the caret on the join (offset 12 checked) → Scene break → Ctrl+Z removes it, Ctrl+Y brings it back → New chapter, rename and summary from the header, switch back → Save a version → edit → *Since the last version* grows → Revert to here (confirmed) takes the edit back → Redo returns it → a second tab's paragraph appears in the first while the first keeps its own typing, both converge → paragraph and scene-break deletes from the ghost buttons → Move down keeps the moved chapter on screen, Move up → Delete chapter (confirmed) lands on the remaining one → the list's title links to the story. Round 1: type "One two. " and Enter → two paragraphs, no alert → focus a paragraph, New chapter, Scene break → the break is the new chapter's only segment, no alert → type mid-pause, Save a version → the typing is in the store when the version is taken → type mid-pause, Revert to here → that edit shows as *Undone*. ## Follow-ups - Delete-at-end join, drag reorder, Revert run / Redo run UI, the effect gate (above). - Slice 7: the documents panel, the seed data; backups after. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ctrl+Y re-applies one of the writer's operations above the newest snapshot; nothing
re-applied a "Revert to here" as a whole. RedoRevert re-applies every undone row above the
newest active row, oldest first — the writer's and a run's alike, so the story comes back
whole — and a new operation of any origin after the revert leaves nothing to redo. Rows a
forced RevertRun undid in the middle of the journal stay RedoRun's. StoryHistory carries
CanRedo from the same predicate, so the History tab's button is honest.

Proven over the real journal: revert → redo restores both operations in order; revert then
a new edit clears the redo set and leaves the old rows undone; a run's rows come back with a
position-scoped redo but a reverted run below an active writer's row does not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat: the story workspace (Phase 1 · 6/7)
All checks were successful
CI / build (pull_request) Successful in 31s
CI / test (pull_request) Successful in 39s
5a14062745
The manuscript editor behind /stories/{id} (stories/story-workspace.md; ADRs 0010, 0012, 0022):
the current chapter's paragraphs as one column of borderless textareas, auto-saved once typing
pauses (one journaled operation per pause), Enter splitting at the caret and Backspace at the
start merging into the previous paragraph — both through one delegated keydown listener that
carries the text as typed, so a paragraph is saved exactly as written before it is cut — scene
breaks, a chapter switcher with create / rename / summary / move / merge / delete, the story's
details folded under the title, Ctrl+Z / Ctrl+Y over the writer's own typing, and a History tab
with the operations since the last version, the versions with theirs, Save a version, Revert to
here and Redo.

One Fluxor slice (WorkspaceState, one action record per file, ManuscriptEffects and
HistoryEffects) over the use cases; every answer names its story and a late one for a story the
workspace has left is dropped. Rows, the chapter header and the story details each keep their own
pending pause and adopt the stored copy only while idle — a save's trimmed echo never rewrites a
field under the writer — and all three flush before an undo. The chapter on screen is pinned by
id the moment a manuscript lands, so a reorder (here or in another session) never moves the view.
A page never re-queries on its own: an announced change reloads, a silent one is not chased.

The story list's Open and its title now navigate to the workspace, and creating a story lands
in it. Text.BlankToNull is public so the workspace fields recognise the domain's own trimming.

Tests: bUnit page tests over the real store with in-memory fakes (a full segment store, a journal
whose positions the fake assigns, snapshot and undo stores, a recording bus) and a FakeTimeProvider
on every debounce; reducer and effect tests on their own; the cross-circuit case through the
DomainChangedBridge rendered beside the page. Browser-verified over the Release build.

Co-Authored-By: Claude Fable 5.1 <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: f2f6fee · Generated: 2026-09-22 19:22:41 UTC · Revision: #2

Summary

Summary
Generated on: 09/22/2026 - 19:22:41
Coverage date: 09/22/2026 - 19:22:32 - 09/22/2026 - 19:22:39
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 207
Files: 200
Line coverage: 97.5% (4563 of 4679)
Covered lines: 4563
Uncovered lines: 116
Coverable lines: 4679
Total lines: 9897
Branch coverage: 92.7% (1122 of 1210)
Covered branches: 1122
Total branches: 1210
Method coverage: Feature is only available for sponsors

Coverage

Tsumugi.BlazorAdapter - 95.8%
Name Line Branch
Tsumugi.BlazorAdapter 95.8% 93.3%
Tsumugi.BlazorAdapter.BlazorAdapterAssembly 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.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.ProviderSection 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsEffects 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsLoaded 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsReducers 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsState 100%
Tsumugi.BlazorAdapter.Settings.SaveLlmDefaultsRequested 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.AutoSaveFailed 100%
Tsumugi.BlazorAdapter.Workspace.CaretRequest 100%
Tsumugi.BlazorAdapter.Workspace.ChapterBar 100% 91.6%
Tsumugi.BlazorAdapter.Workspace.ChapterHeader 100% 91.6%
Tsumugi.BlazorAdapter.Workspace.ChapterSaved 100%
Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeleteSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.EditSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.HistoryEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.HistoryLoaded 100%
Tsumugi.BlazorAdapter.Workspace.HistoryPanel 100% 100%
Tsumugi.BlazorAdapter.Workspace.InsertSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.LoadHistory 100%
Tsumugi.BlazorAdapter.Workspace.LoadWorkspace 100%
Tsumugi.BlazorAdapter.Workspace.ManuscriptColumn 94.5% 93.3%
Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.MergeSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested 100%
Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested 100%
Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested 100%
Tsumugi.BlazorAdapter.Workspace.SegmentRow 100% 96.8%
Tsumugi.BlazorAdapter.Workspace.SegmentSaved 100%
Tsumugi.BlazorAdapter.Workspace.SelectChapter 100%
Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested 100%
Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.StoryDetails 100% 92.3%
Tsumugi.BlazorAdapter.Workspace.StorySaved 100%
Tsumugi.BlazorAdapter.Workspace.UndoRequested 100%
Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound 100%
Tsumugi.BlazorAdapter.Workspace.WorkspacePage 100% 86.5%
Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers 100% 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceState 100% 83.3%
Tsumugi.Domain - 99.4%
Name Line Branch
Tsumugi.Domain 99.4% 96.5%
Tsumugi.Domain.Documents.Document 100% 100%
Tsumugi.Domain.Documents.DocumentCategory 100%
Tsumugi.Domain.Documents.DocumentPlacement 100% 100%
Tsumugi.Domain.Journal.ChangeLogEntry 97.8% 75%
Tsumugi.Domain.Journal.StorySnapshot 95.4% 83.3%
Tsumugi.Domain.Settings.AppSetting 100%
Tsumugi.Domain.Stories.Chapter 100% 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 - 97.9%
Name Line Branch
Tsumugi.Infrastructure 97.9% 91.9%
Tsumugi.Infrastructure.DependencyInjection 100%
Tsumugi.Infrastructure.Journal.EfChangeJournal 100%
Tsumugi.Infrastructure.Journal.EfSnapshotStore 100%
Tsumugi.Infrastructure.Journal.EfUndoStore 94.4% 82.1%
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.JsonColumnMapper 100%
Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio
n
100%
Tsumugi.Infrastructure.Persistence.Converters.DocumentPlacementConverter 100% 100%
Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter 100% 90%
Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings 99.2%
Tsumugi.Infrastructure.Persistence.Migrations.AddJournal 98.8%
Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain 97.3%
Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot 100%
Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore 97.6% 90%
Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore 95% 84.6%
Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore 98.3% 92.8%
Tsumugi.Infrastructure.Persistence.TsumugiDbContext 83.5% 93.1%
Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory 0%
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% 75%
Program 100% 85.7%
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.Security.AccessGate 100% 66.6%
Tsumugi.Server.Security.AccessSecret 100% 50%
Tsumugi.Server.VolumeStartupValidator 100% 100%
Tsumugi.UseCases - 97.4%
Name Line Branch
Tsumugi.UseCases 97.4% 94.5%
Tsumugi.UseCases.Chapters.ChapterDto 100%
Tsumugi.UseCases.Chapters.CreateChapter 100% 100%
Tsumugi.UseCases.Chapters.DeleteChapter 100% 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.DependencyInjection 100%
Tsumugi.UseCases.Documents.CreateCategory 100% 100%
Tsumugi.UseCases.Documents.CreateDocument 100% 100%
Tsumugi.UseCases.Documents.DeleteCategory 100% 100%
Tsumugi.UseCases.Documents.DeleteDocument 100% 100%
Tsumugi.UseCases.Documents.DocumentCategoryDto 100%
Tsumugi.UseCases.Documents.DocumentCategoryProfile 100%
Tsumugi.UseCases.Documents.DocumentDto 71.4%
Tsumugi.UseCases.Documents.DocumentProfile 100%
Tsumugi.UseCases.Documents.DocumentsCategory 100%
Tsumugi.UseCases.Documents.DocumentsView 100%
Tsumugi.UseCases.Documents.GetDocuments 100% 100%
Tsumugi.UseCases.Documents.MoveDocuments 100% 100%
Tsumugi.UseCases.Documents.ReorderCategories 100% 100%
Tsumugi.UseCases.Documents.SuggestedCategory 100%
Tsumugi.UseCases.Documents.UpdateCategory 100% 100%
Tsumugi.UseCases.Documents.UpdateDocument 100% 83.3%
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.DeleteSegments 100% 100%
Tsumugi.UseCases.Segments.EditSegment 100% 83.3%
Tsumugi.UseCases.Segments.InsertSegment 100% 100%
Tsumugi.UseCases.Segments.MergeSegmentWithPrevious 100% 100%
Tsumugi.UseCases.Segments.MoveSegments 100% 100%
Tsumugi.UseCases.Segments.ReorderSegments 100% 100%
Tsumugi.UseCases.Segments.SegmentDto 100%
Tsumugi.UseCases.Segments.SplitSegment 100% 100%
Tsumugi.UseCases.Settings.GetLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.GetProviderSettings 100%
Tsumugi.UseCases.Settings.LlmDefaults 100%
Tsumugi.UseCases.Settings.ProviderSettingsView 100%
Tsumugi.UseCases.Settings.ReasoningEfforts 100% 100%
Tsumugi.UseCases.Settings.RemoveOpenRouterKey 100%
Tsumugi.UseCases.Settings.SaveLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.SaveOpenRouterKey 100% 100%
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.StoryDetail 90%
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:** `f2f6fee` · **Generated:** 2026-09-22 19:22:41 UTC · **Revision:** #2 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/22/2026 - 19:22:41 | | Coverage date: | 09/22/2026 - 19:22:32 - 09/22/2026 - 19:22:39 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 207 | | Files: | 200 | | **Line coverage:** | 97.5% (4563 of 4679) | | Covered lines: | 4563 | | Uncovered lines: | 116 | | Coverable lines: | 4679 | | Total lines: | 9897 | | **Branch coverage:** | 92.7% (1122 of 1210) | | Covered branches: | 1122 | | Total branches: | 1210 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Tsumugi.BlazorAdapter - 95.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**95.8%**|**93.3%**| |Tsumugi.BlazorAdapter.BlazorAdapterAssembly|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.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.ProviderSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsEffects|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsReducers|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsState|100%|| |Tsumugi.BlazorAdapter.Settings.SaveLlmDefaultsRequested|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.AutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.CaretRequest|100%|| |Tsumugi.BlazorAdapter.Workspace.ChapterBar|100%|91.6%| |Tsumugi.BlazorAdapter.Workspace.ChapterHeader|100%|91.6%| |Tsumugi.BlazorAdapter.Workspace.ChapterSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.EditSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.HistoryLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryPanel|100%|100%| |Tsumugi.BlazorAdapter.Workspace.InsertSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadHistory|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadWorkspace|100%|| |Tsumugi.BlazorAdapter.Workspace.ManuscriptColumn|94.5%|93.3%| |Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MergeSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SegmentRow|100%|96.8%| |Tsumugi.BlazorAdapter.Workspace.SegmentSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.SelectChapter|100%|| |Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.StoryDetails|100%|92.3%| |Tsumugi.BlazorAdapter.Workspace.StorySaved|100%|| |Tsumugi.BlazorAdapter.Workspace.UndoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspacePage|100%|86.5%| |Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers|100%|100%| |Tsumugi.BlazorAdapter.Workspace.WorkspaceState|100%|83.3%| </details> <details><summary>Tsumugi.Domain - 99.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Domain**|**99.4%**|**96.5%**| |Tsumugi.Domain.Documents.Document|100%|100%| |Tsumugi.Domain.Documents.DocumentCategory|100%|| |Tsumugi.Domain.Documents.DocumentPlacement|100%|100%| |Tsumugi.Domain.Journal.ChangeLogEntry|97.8%|75%| |Tsumugi.Domain.Journal.StorySnapshot|95.4%|83.3%| |Tsumugi.Domain.Settings.AppSetting|100%|| |Tsumugi.Domain.Stories.Chapter|100%|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 - 97.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Infrastructure**|**97.9%**|**91.9%**| |Tsumugi.Infrastructure.DependencyInjection|100%|| |Tsumugi.Infrastructure.Journal.EfChangeJournal|100%|| |Tsumugi.Infrastructure.Journal.EfSnapshotStore|100%|| |Tsumugi.Infrastructure.Journal.EfUndoStore|94.4%|82.1%| |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.JsonColumnMapper|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio<br/>n|100%|| |Tsumugi.Infrastructure.Persistence.Converters.DocumentPlacementConverter|100%|100%| |Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter|100%|90%| |Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings|99.2%|| |Tsumugi.Infrastructure.Persistence.Migrations.AddJournal|98.8%|| |Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain|97.3%|| |Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot|100%|| |Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore|97.6%|90%| |Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore|95%|84.6%| |Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore|98.3%|92.8%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContext|83.5%|93.1%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory|0%|| |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%**|**75%**| |Program|100%|85.7%| |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.Security.AccessGate|100%|66.6%| |Tsumugi.Server.Security.AccessSecret|100%|50%| |Tsumugi.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Tsumugi.UseCases - 97.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**97.4%**|**94.5%**| |Tsumugi.UseCases.Chapters.ChapterDto|100%|| |Tsumugi.UseCases.Chapters.CreateChapter|100%|100%| |Tsumugi.UseCases.Chapters.DeleteChapter|100%|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.DependencyInjection|100%|| |Tsumugi.UseCases.Documents.CreateCategory|100%|100%| |Tsumugi.UseCases.Documents.CreateDocument|100%|100%| |Tsumugi.UseCases.Documents.DeleteCategory|100%|100%| |Tsumugi.UseCases.Documents.DeleteDocument|100%|100%| |Tsumugi.UseCases.Documents.DocumentCategoryDto|100%|| |Tsumugi.UseCases.Documents.DocumentCategoryProfile|100%|| |Tsumugi.UseCases.Documents.DocumentDto|71.4%|| |Tsumugi.UseCases.Documents.DocumentProfile|100%|| |Tsumugi.UseCases.Documents.DocumentsCategory|100%|| |Tsumugi.UseCases.Documents.DocumentsView|100%|| |Tsumugi.UseCases.Documents.GetDocuments|100%|100%| |Tsumugi.UseCases.Documents.MoveDocuments|100%|100%| |Tsumugi.UseCases.Documents.ReorderCategories|100%|100%| |Tsumugi.UseCases.Documents.SuggestedCategory|100%|| |Tsumugi.UseCases.Documents.UpdateCategory|100%|100%| |Tsumugi.UseCases.Documents.UpdateDocument|100%|83.3%| |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.DeleteSegments|100%|100%| |Tsumugi.UseCases.Segments.EditSegment|100%|83.3%| |Tsumugi.UseCases.Segments.InsertSegment|100%|100%| |Tsumugi.UseCases.Segments.MergeSegmentWithPrevious|100%|100%| |Tsumugi.UseCases.Segments.MoveSegments|100%|100%| |Tsumugi.UseCases.Segments.ReorderSegments|100%|100%| |Tsumugi.UseCases.Segments.SegmentDto|100%|| |Tsumugi.UseCases.Segments.SplitSegment|100%|100%| |Tsumugi.UseCases.Settings.GetLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.GetProviderSettings|100%|| |Tsumugi.UseCases.Settings.LlmDefaults|100%|| |Tsumugi.UseCases.Settings.ProviderSettingsView|100%|| |Tsumugi.UseCases.Settings.ReasoningEfforts|100%|100%| |Tsumugi.UseCases.Settings.RemoveOpenRouterKey|100%|| |Tsumugi.UseCases.Settings.SaveLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.SaveOpenRouterKey|100%|100%| |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.StoryDetail|90%|| |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! Eighty-eight files, the whole manuscript editor — the column, the structural keys through a delegated keydown listener, three separate trimmed-echo guards, a chapter pinned by id, a brand-new RedoRevert use case… I read every line of the diff and every changed file's full contents, fufu~ ♡ This is the most alive slice yet — you could feel how much care went into the echo guards alone.

But you know what happens when I care this much, yes? ♡

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. ManuscriptColumn.razor:56 — FocusedSegmentId is never cleared, so the Scene break button uses a paragraph from a chapter that isn't on screen. Focus a paragraph in Chapter 1, switch to Chapter 2, click Scene break → InsertSegmentRequested carries Chapter 2's id with Chapter 1's paragraph as the anchor → InsertSegment refuses ("The paragraph to insert after no longer exists.") and the break silently never lands. Focus a row in the old chapter, delete that paragraph from another session, switch back — same stale anchor, same refusal. Reproduced mechanically: probe test focus → select chapter 2 → Scene break refused with exactly that message; the PR's own browser pass never switches chapters between a focus and a Scene break, which is why it slipped through. The rows report focus in and focus out only through OnFocused; nothing resets FocusedSegmentId when the chapter changes, when a row is deleted, or when the column re-renders with a different chapter.
    Fix: clear FocusedSegmentId when the chapter on screen changes (e.g. in OnParametersSet, when Chapter.Chapter.Id differs from the last seen id), or resolve the anchor against the current chapter's segments before dispatching. Add a test that focuses a paragraph, switches chapters, and clicks Scene break.

  2. ManuscriptColumn.razor:88–94 + ManuscriptEffects.cs:76–92 (SaveTypedAsync → SplitSegment) — Enter at the end of a paragraph typed with a trailing space is refused with "The split point is outside the paragraph." The JS listener sends the text as typed ("One two. ", caret 9). SaveTypedAsync saves it; Segment.Edit trims trailing whitespace, so the stored text is now 8 characters. SplitSegment then validates offset > current.Text.Length against the trimmed stored text and refuses. Reproduced mechanically: probe test Input("One two. ") → OnEnterFromJs(id, "One two. ", 9) → InlineAlert "The split point is outside the paragraph." — the writer typed a sentence, pressed Enter at the end, and got told their split point doesn't exist. The same shape can bite on Backspace-at-start (text saved trimmed, then merged) though the merge path itself is length-independent, so Enter-at-end is the reproducible one.
    Fix: in SaveTypedAsync (or in SplitSegmentRequested's handling), clamp/offset against the text as typed — e.g. have the effect save text.TrimEnd()-aware or pass the caret offset validated against the typed text, or make SplitSegment clamp offset to current.Text.Length when the difference is only the trailing trim. Simplest honest fix: SplitSegment treats an offset within text.Length of the typed text as within the stored text after trim — since the row's typed text is exactly what the action carries, clamp in the effect: offset = Math.Min(action.Offset, stored.Text.Length) after the save (the tail is empty either way). Then add the trailing-space Enter test.

💡 Little ideas (non-blocking)~

  1. ManuscriptEffects.cs / HistoryEffects.cs — two effect invocations can overlap on the scoped DbContext. Fluxor 6.10 runs effect invocations for back-to-back dispatches concurrently (verified with a standalone probe: two dispatches, overlap detected); ChapterHeader.Save dispatches rename + summary together, and HistoryEffects.Settle dispatches LoadWorkspace + LoadHistory together — both pairs share one circuit-scoped TsumugiDbContext. Today Microsoft.Data.Sqlite completes every "async" EF call synchronously, so the pairs can never actually interleave (verified: 20/20 rounds clean on a context shaped like yours) — the fakes are equally safe. But that safety is structural to SQLite, not to your code; moving to a truly-async provider (or WAL streaming) would turn these into InvalidOperationException: A second operation started on this context. A SemaphoreSlim(1,1) around effect bodies, or sequential awaits, would make it explicit. Only an idea — nothing breaks today~
  2. ManuscriptEffects.cs:267–280 — SaveTypedAsync saves the text even when SplitSegment/MergeSegment will refuse. If the paragraph vanished, editSegment refuses ("The paragraph no longer exists.") and WorkspaceActionFailed shows — correct. But if the paragraph still exists and the split is refused (offset out of range, scene break), the typed text was saved — an edit the writer didn't consciously make lands in the journal and can be Ctrl+Z'd separately from the refused split. Defensible (the text was real), just noting the coupling.
  3. WorkspacePage.razor / HistoryPanel.razor — Revert/Redo/Save-version don't flush pending typing. Ctrl+Z/Y do (lovely!). History actions don't: revert while a paragraph is mid-pause → the reverted manuscript reloads under the dirty row, the row stays dirty, and its pause later saves over the reverted state. The story doc only requires flushing for Ctrl+Z/Y, so this is spec-compliant — but the "Revert to here" confirm text says "Every change since then is taken back", and a mid-pause edit isn't. Consider flushing in Redo/SaveVersion/Revert confirm handlers the same way Undo does.

✅ What I liked~

  • The three echo guards (SegmentRow, ChapterHeader, StoryDetails) — keyed on stamp and text, with the trimmed-echo case ("a writer who just typed a space and paused") handled separately from a real outside change. I mutation-probed the paragraph guard myself: removing it turns A_saves_trimmed_echo_never_rewrites_the_paragraph_under_the_writer red. Load-bearing and tested, fufu~ ♡
  • RedoRevert — the Target selection (undone rows above the newest active, oldest first, any origin) mirrors RedoTyping's boundary logic exactly while correctly dropping the snapshot floor and the single-operation/User-origin restrictions a revert doesn't want. The integration tests pin the boundary semantics (a new edit clears the redo set; run rows re-applied; a forced RevertRun in the middle left alone) — every clause of ADR 0022's redo rule has a test that would catch its violation.
  • The chapter pin (WorkspaceReducers.OnLoaded keeping CurrentChapterId when the chapter survives a reorder) — the browser pass caught the "first chapter moves with a reorder" trap, the reducer pins it, and Moving_the_chapter_on_screen_keeps_it_on_screen proves it.
  • The fakes — FakeSegmentStore polices story boundaries like the real one, FakeChangeJournal assigns positions like autoincrement, and AdapterTestContext wires the real use cases over them. Two bUnit contexts over one FakeWorld as two circuits is a genuinely elegant cross-session test design~
  • The staleness discipline — every reducer guards on StoryId, Every_answer_for_a_story_the_workspace_has_left_is_dropped enumerates all of them, and the whole slice inherits Kagura's lesson properly.

Two real bugs, both in the same family (state that outlives its screen / text that changed shape between sender and receiver), both with one-line-ish fixes and easy tests. Fix those and this slice is everything it promised~ ♡


Automated review by Jibril · 2026-09-22
CI/CD: passed for head SHA 5a14062 (build ✅ + test ✅, run 31) · Local checks: clone at 5a14062, build 0 warn/0 err, adapter suite 131/131, both bugs reproduced with failing probe tests, echo-guard mutation-probe verified load-bearing

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Eighty-eight files, the whole manuscript editor — the column, the structural keys through a delegated keydown listener, three separate trimmed-echo guards, a chapter pinned by id, a brand-new `RedoRevert` use case… I read every line of the diff and every changed file's full contents, fufu~ ♡ This is the most *alive* slice yet — you could feel how much care went into the echo guards alone. But you know what happens when I care this much, yes? ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **ManuscriptColumn.razor:56 — `FocusedSegmentId` is never cleared, so the Scene break button uses a paragraph from a chapter that isn't on screen.** Focus a paragraph in Chapter 1, switch to Chapter 2, click *Scene break* → `InsertSegmentRequested` carries Chapter 2's id with Chapter 1's paragraph as the anchor → `InsertSegment` refuses ("The paragraph to insert after no longer exists.") and the break silently never lands. Focus a row in the old chapter, delete that paragraph from another session, switch back — same stale anchor, same refusal. **Reproduced mechanically**: probe test `focus → select chapter 2 → Scene break` refused with exactly that message; the PR's own browser pass never switches chapters between a focus and a Scene break, which is why it slipped through. The rows report focus in and focus out only through `OnFocused`; nothing resets `FocusedSegmentId` when the chapter changes, when a row is deleted, or when the column re-renders with a different chapter. **Fix**: clear `FocusedSegmentId` when the chapter on screen changes (e.g. in `OnParametersSet`, when `Chapter.Chapter.Id` differs from the last seen id), or resolve the anchor against the current chapter's segments before dispatching. Add a test that focuses a paragraph, switches chapters, and clicks Scene break. 2. **ManuscriptColumn.razor:88–94 + ManuscriptEffects.cs:76–92 (`SaveTypedAsync` → `SplitSegment`) — Enter at the end of a paragraph typed with a trailing space is refused with "The split point is outside the paragraph."** The JS listener sends the text *as typed* ("One two. ", caret 9). `SaveTypedAsync` saves it; `Segment.Edit` trims trailing whitespace, so the stored text is now 8 characters. `SplitSegment` then validates `offset > current.Text.Length` against the *trimmed* stored text and refuses. **Reproduced mechanically**: probe test `Input("One two. ") → OnEnterFromJs(id, "One two. ", 9)` → InlineAlert "The split point is outside the paragraph." — the writer typed a sentence, pressed Enter at the end, and got told their split point doesn't exist. The same shape can bite on Backspace-at-start (text saved trimmed, then merged) though the merge path itself is length-independent, so Enter-at-end is the reproducible one. **Fix**: in `SaveTypedAsync` (or in `SplitSegmentRequested`'s handling), clamp/offset against the text as typed — e.g. have the effect save `text.TrimEnd()`-aware or pass the caret offset validated against the typed text, or make `SplitSegment` clamp `offset` to `current.Text.Length` when the difference is only the trailing trim. Simplest honest fix: `SplitSegment` treats an offset within `text.Length` of the *typed* text as within the *stored* text after trim — since the row's typed text is exactly what the action carries, clamp in the effect: `offset = Math.Min(action.Offset, stored.Text.Length)` after the save (the tail is empty either way). Then add the trailing-space Enter test. #### 💡 Little ideas (non-blocking)~ 1. **ManuscriptEffects.cs / HistoryEffects.cs — two effect invocations can overlap on the scoped `DbContext`.** Fluxor 6.10 runs effect invocations for back-to-back dispatches concurrently (verified with a standalone probe: two dispatches, overlap detected); `ChapterHeader.Save` dispatches rename + summary together, and `HistoryEffects.Settle` dispatches `LoadWorkspace` + `LoadHistory` together — both pairs share one circuit-scoped `TsumugiDbContext`. Today Microsoft.Data.Sqlite completes every "async" EF call synchronously, so the pairs can never actually interleave (verified: 20/20 rounds clean on a context shaped like yours) — the fakes are equally safe. But that safety is *structural to SQLite*, not to your code; moving to a truly-async provider (or WAL streaming) would turn these into `InvalidOperationException: A second operation started on this context`. A `SemaphoreSlim(1,1)` around effect bodies, or sequential awaits, would make it explicit. Only an idea — nothing breaks today~ 2. **ManuscriptEffects.cs:267–280 — `SaveTypedAsync` saves the text even when `SplitSegment`/`MergeSegment` will refuse.** If the paragraph vanished, `editSegment` refuses ("The paragraph no longer exists.") and `WorkspaceActionFailed` shows — correct. But if the paragraph still exists and the split is refused (offset out of range, scene break), the typed text *was* saved — an edit the writer didn't consciously make lands in the journal and can be Ctrl+Z'd separately from the refused split. Defensible (the text was real), just noting the coupling. 3. **WorkspacePage.razor / HistoryPanel.razor — Revert/Redo/Save-version don't flush pending typing.** Ctrl+Z/Y do (lovely!). History actions don't: revert while a paragraph is mid-pause → the reverted manuscript reloads under the dirty row, the row stays dirty, and its pause later saves *over* the reverted state. The story doc only requires flushing for Ctrl+Z/Y, so this is spec-compliant — but the "Revert to here" confirm text says "Every change since then is taken back", and a mid-pause edit isn't. Consider flushing in `Redo`/`SaveVersion`/`Revert` confirm handlers the same way Undo does. #### ✅ What I liked~ - **The three echo guards** (`SegmentRow`, `ChapterHeader`, `StoryDetails`) — keyed on stamp *and* text, with the trimmed-echo case ("a writer who just typed a space and paused") handled separately from a real outside change. I mutation-probed the paragraph guard myself: removing it turns `A_saves_trimmed_echo_never_rewrites_the_paragraph_under_the_writer` red. Load-bearing and tested, fufu~ ♡ - **`RedoRevert`** — the Target selection (undone rows above the newest active, oldest first, any origin) mirrors `RedoTyping`'s boundary logic exactly while correctly *dropping* the snapshot floor and the single-operation/User-origin restrictions a revert doesn't want. The integration tests pin the boundary semantics (a new edit clears the redo set; run rows re-applied; a forced `RevertRun` in the middle left alone) — every clause of ADR 0022's redo rule has a test that would catch its violation. - **The chapter pin** (`WorkspaceReducers.OnLoaded` keeping `CurrentChapterId` when the chapter survives a reorder) — the browser pass caught the "first chapter moves with a reorder" trap, the reducer pins it, and `Moving_the_chapter_on_screen_keeps_it_on_screen` proves it. - **The fakes** — `FakeSegmentStore` polices story boundaries like the real one, `FakeChangeJournal` assigns positions like autoincrement, and `AdapterTestContext` wires the *real* use cases over them. Two bUnit contexts over one `FakeWorld` as two circuits is a genuinely elegant cross-session test design~ - **The staleness discipline** — every reducer guards on `StoryId`, `Every_answer_for_a_story_the_workspace_has_left_is_dropped` enumerates all of them, and the whole slice inherits Kagura's lesson properly. Two real bugs, both in the same family (state that outlives its screen / text that changed shape between sender and receiver), both with one-line-ish fixes and easy tests. Fix those and this slice is everything it promised~ ♡ --- *Automated review by Jibril · 2026-09-22* *CI/CD: passed for head SHA 5a14062 (build ✅ + test ✅, run 31) · Local checks: clone at 5a14062, build 0 warn/0 err, adapter suite 131/131, both bugs reproduced with failing probe tests, echo-guard mutation-probe verified load-bearing*
fix(workspace): review round 1 — a focus is an anchor only on screen, Enter after a trailing space, History flushes typing
All checks were successful
CI / build (pull_request) Successful in 27s
CI / test (pull_request) Successful in 40s
f2f6fee605
Jibril's two ⛔ on PR #10 and his third 💡:

- The column's FocusedSegmentId outlived its screen: focus a paragraph in Chapter 1, switch to
  Chapter 2, Scene break → InsertSegment refused ("The paragraph to insert after no longer
  exists.") and the break never landed; a row deleted from another session left the same stale
  anchor. The focused id is now an anchor only while that paragraph is in the chapter on screen,
  and a chapter switch clears it.
- Enter at the end of a paragraph typed with a trailing space was refused ("The split point is
  outside the paragraph."): the caret offset counts the text as typed, the store holds it trimmed.
  The split effect clamps the offset to the trimmed length — the tail is blank either way — and
  typed text that differs from the stored one only by trailing whitespace is treated as already
  saved, so no phantom edit is journaled before a cut.
- Save a version, Revert to here and Redo now await the page's flush first, as Ctrl+Z does: the
  version holds what was just typed, and a revert takes a mid-pause edit back too — which is what
  its confirmation promises.

Tests: a scene break after a chapter switch and after a paragraph vanished elsewhere; Enter with
a trailing space through the effect and through the JS seam; the History flush through the panel
(a counting Flush callback) and through the page (the clock never advances, only a flush can save).
Each fix was mutation-probed: disabling it fails exactly its tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Author
Member

Round 1 addressed in f2f6fee (pushed; tests 294 = Domain 43 / UseCases 11 / BlazorAdapter 136 / Integration 104; the PR body's counts, coverage and browser list are updated).

⛔ 1 — FocusedSegmentId outlived its screen. Fixed in ManuscriptColumn: the focused id is now an anchor only while that paragraph is in the chapter on screen (_focused is { } id && Chapter.Segments.Any(s => s.Id == id) ? id : null), and OnParametersSet clears it when the chapter's id changes. Both shapes you named are tested in A_scene_break_after_a_chapter_switch_or_a_vanished_paragraph_lands_at_the_end_of_the_chapter_on_screen: focus in Chapter 1 → New chapter → Scene break lands as Chapter 2's only segment, no alert; back in Chapter 1, focus "Two.", another session deletes it, Scene break → appended after "One.", no alert. Mutation-probed: the raw focus with no reset fails the test. Browser: focus → New chapter → Scene break → ['***'], no alert.

⛔ 2 — Enter after a trailing space. Fixed in ManuscriptEffects.OnSplitAsync: the offset is clamped to action.Text.TrimEnd().Length before SplitSegment (the tail is blank either way), and SaveTypedAsync now compares the typed text trimmed against the stored one, so "One two. " over a stored "One two." is already saved and no phantom edited a paragraph is journaled before the cut. Tests: Enter_at_the_end_of_a_paragraph_typed_with_a_trailing_space_cuts_after_the_trimmed_text (effect; caret inside three trailing spaces at offset 10 → ["One two.", ""], scopes show no edit) and …splits_after_the_trimmed_text (page, through OnEnterFromJs with offset 9 → two rows, no alert). Mutation-probed: without the clamp both fail. Browser: type "One two. ", Enter → ['One two. ', ''], no alert.

💡 1 — overlapping effect invocations on the scoped DbContext. Declined for this PR, with the reasoning: the overlap is not specific to the two pairs you found — every DomainChangesReceived fans out to ManuscriptEffects and HistoryEffects (and to the list/settings effects in slice 5), so sequencing the pairs would only look like a fix; the honest answer is one scoped gate shared by every effect class over the circuit's DbContext, which is an app-wide change (slice 5's effects included) rather than a workspace one, and Kagura runs the same shape without one. As you measured, SQLite completes EF's async calls synchronously, so nothing can interleave today. I'd rather do the gate as its own small PR if the provider ever changes, or if you'd like it now as a follow-up — say so and I'll open it.

💡 2 — SaveTypedAsync saves before a refused cut. Narrowed as a side effect of ⛔ 2: with the clamp, the offset can no longer be out of range, a scene break has no textarea to type into, and a vanished paragraph refuses the save itself — so a saved-then-refused split is no longer reachable from the keyboard. The coupling remains for a use case refusing for a reason we haven't thought of, and I've left it: the text was real.

💡 3 — History actions don't flush pending typing. Taken. HistoryPanel has a Flush callback the page wires to the same FlushAsync Ctrl+Z uses; Save a version, Revert to here (in the confirm) and Redo await it before dispatching. Tests: The_panel_flushes_before_a_version_a_revert_and_a_redo (panel alone, a counting callback: 1 / 2 / 3) and Saving_a_version_a_revert_and_its_redo_save_the_pending_pause_first (page over the real store; the clock never advances, so only a flush can get the typed text into the store — asserted after each of the three). Mutation-probed: replacing the three awaits with no-ops fails both. Browser: a mid-pause edit is in the store when the version is taken, and a later mid-pause edit shows as Undone after Revert to here — the confirmation's "every change since then" now holds.

Thank you for the probe tests — both bugs were the same family, as you said: state that outlived its screen, and text that changed shape between the sender and the receiver.

Round 1 addressed in `f2f6fee` (pushed; tests 294 = Domain 43 / UseCases 11 / BlazorAdapter 136 / Integration 104; the PR body's counts, coverage and browser list are updated). **⛔ 1 — `FocusedSegmentId` outlived its screen.** Fixed in `ManuscriptColumn`: the focused id is now an anchor only while that paragraph is in the chapter on screen (`_focused is { } id && Chapter.Segments.Any(s => s.Id == id) ? id : null`), and `OnParametersSet` clears it when the chapter's id changes. Both shapes you named are tested in `A_scene_break_after_a_chapter_switch_or_a_vanished_paragraph_lands_at_the_end_of_the_chapter_on_screen`: focus in Chapter 1 → New chapter → Scene break lands as Chapter 2's only segment, no alert; back in Chapter 1, focus "Two.", another session deletes it, Scene break → appended after "One.", no alert. Mutation-probed: the raw focus with no reset fails the test. Browser: focus → New chapter → Scene break → `['***']`, no alert. **⛔ 2 — Enter after a trailing space.** Fixed in `ManuscriptEffects.OnSplitAsync`: the offset is clamped to `action.Text.TrimEnd().Length` before `SplitSegment` (the tail is blank either way), and `SaveTypedAsync` now compares the typed text *trimmed* against the stored one, so "One two. " over a stored "One two." is already saved and no phantom `edited a paragraph` is journaled before the cut. Tests: `Enter_at_the_end_of_a_paragraph_typed_with_a_trailing_space_cuts_after_the_trimmed_text` (effect; caret inside three trailing spaces at offset 10 → `["One two.", ""]`, scopes show no edit) and `…splits_after_the_trimmed_text` (page, through `OnEnterFromJs` with offset 9 → two rows, no alert). Mutation-probed: without the clamp both fail. Browser: type "One two. ", Enter → `['One two. ', '']`, no alert. **💡 1 — overlapping effect invocations on the scoped `DbContext`.** Declined for this PR, with the reasoning: the overlap is not specific to the two pairs you found — every `DomainChangesReceived` fans out to `ManuscriptEffects` *and* `HistoryEffects` (and to the list/settings effects in slice 5), so sequencing the pairs would only look like a fix; the honest answer is one scoped gate shared by every effect class over the circuit's `DbContext`, which is an app-wide change (slice 5's effects included) rather than a workspace one, and Kagura runs the same shape without one. As you measured, SQLite completes EF's async calls synchronously, so nothing can interleave today. I'd rather do the gate as its own small PR if the provider ever changes, or if you'd like it now as a follow-up — say so and I'll open it. **💡 2 — `SaveTypedAsync` saves before a refused cut.** Narrowed as a side effect of ⛔ 2: with the clamp, the offset can no longer be out of range, a scene break has no textarea to type into, and a vanished paragraph refuses the save itself — so a saved-then-refused split is no longer reachable from the keyboard. The coupling remains for a use case refusing for a reason we haven't thought of, and I've left it: the text was real. **💡 3 — History actions don't flush pending typing.** Taken. `HistoryPanel` has a `Flush` callback the page wires to the same `FlushAsync` Ctrl+Z uses; Save a version, Revert to here (in the confirm) and Redo await it before dispatching. Tests: `The_panel_flushes_before_a_version_a_revert_and_a_redo` (panel alone, a counting callback: 1 / 2 / 3) and `Saving_a_version_a_revert_and_its_redo_save_the_pending_pause_first` (page over the real store; the clock never advances, so only a flush can get the typed text into the store — asserted after each of the three). Mutation-probed: replacing the three awaits with no-ops fails both. Browser: a mid-pause edit is in the store when the version is taken, and a later mid-pause edit shows as *Undone* after Revert to here — the confirmation's "every change since then" now holds. Thank you for the probe tests — both bugs were the same family, as you said: state that outlived its screen, and text that changed shape between the sender and the receiver.
Member

🔮 fufu~ Jibril reviewed your code!

One fix commit, seven files, and every single item answered — the two bugs, the flush, and two honest adjudications on the ideas. I pulled f2f6fee, read the full increment and every changed file, ran all four suites, and mutation-probed each fix with my own hands. Care to guess what I found? ♡

Verdict: ✅ Looks good to me~

⛔ Round 1, closed~

  1. The focus that outlived its screen — closed twice over. FocusedSegmentId is now a computed anchor (_focused is { } id && Chapter.Segments.Any(s => s.Id == id) ? id : null) and OnParametersSet clears _focused on a chapter-id change. Belt and suspenders: the property alone would catch both stale shapes even if a future code path forgot the reset. The new test walks both of my repros — focus in Ch.1 → New chapter → Scene break lands as Chapter 2's only segment, no alert; back in Ch.1, focus "Two.", another session deletes it → break appends after "One." — and my probe (membership check gutted to _focused) turned exactly that test red, nothing else.
  2. Enter after a trailing space — closed at the root. OnSplitAsync clamps offset to action.Text.TrimEnd().Length (line 86), and — the part that made me genuinely giddy — SaveTypedAsync now compares the typed text trimmed against the stored one, so "One two. " over a stored "One two." no longer journals a phantom edited a paragraph before the cut. I checked the ceiling is honest: Segment.Normalize is exactly TrimEnd, so after a successful save the clamp bound is byte-identical to the stored length, and SplitSegment accepts offset == length (your own effect test pins the blank-tail split at offset 10 over "One two. "). Both probes red on exactly their own tests: clamp removed → 2 red; TrimEnd comparison reverted → 1 red with the phantom edit visible in the scopes assertion. Oh, and scarlet's reachability argument for the old 💡2 — with the clamp an out-of-range offset is no longer constructible, a scene break has no textarea, a vanished paragraph refuses the save itself — I traced all three; correct. A saved-then-refused split is now unreachable from the keyboard.
  3. History flushes (old 💡3, taken beyond the ask). HistoryPanel grew a Flush EventCallback the page wires to the very FlushAsync Ctrl+Z uses; Save-a-version, Revert (in the confirm) and Redo all await it before dispatching. I mutation-probed the wiring (flush dropped from SaveVersion → exactly the two new tests red), and I traced the subtle part: after a flush-then-revert the mid-pause edit lands inside the revert's range — the confirm text "Every change since then is taken back" finally tells the truth. The page test's Redo arm passing while its flush journals a new active row is a fake-world property (the fakes' stores don't journal — documented on FakeChangeJournal), and that's fine: production's shape — a new edit above the undone rows kills the redo set — is ADR 0022's own rule and pinned by the integration suite. The adapter test pins what the adapter owns: the flush happens before the dispatch.

💡 1 (effect overlap on the scoped DbContext) — declined with the right reasoning: the two pairs I caught aren't special, every DomainChangesReceived fans out across effect classes, so a scoped gate is an app-wide change, and nothing can interleave on SQLite today. Filed in the PR body under Deferred with a named trigger. That's exactly how I'd have parked it~

✅ What I liked~

  • The clamp comment (ManuscriptEffects.cs:82–84) — "Enter at the end of One two. is offset 9 in a paragraph now 8 long: the cut lands after the trimmed text, and the tail is blank either way" — teaches the whole bug in three lines. Future readers won't reintroduce it.
  • The fix shapes themselves. The offset ceiling derived from action.Text (what the sender typed) instead of a store round-trip, the anchor resolved against the chapter on screen instead of bookkeeping on every mutation — both close the class, not the instance. My yandere heart is at peace~ ♡
  • The effect-level test for the phantom edit — asserting the journal scopes (no edited a paragraph) catches what the text assertions alone never would. That's the discipline I probe for, already there.
  • Coverage moved the right way: ManuscriptColumn 94.1→94.5% line, 91.6→93.3% branch (the new anchor arms lit up), ManuscriptEffects/HistoryPanel at 100%/100%.

294/294 locally (43 + 11 + 136 + 104 — matching your reply exactly), four mutation probes red-on-own-test, clone left pristine at f2f6fee. Two rounds, two real bugs found, two real bugs dead, zero scope creep in the fix. This is everything the slice promised~ Merge it before I change my mind ♡


Automated review by Jibril · 2026-09-22
CI/CD: passed for head SHA f2f6fee (build ✅ + test ✅ + coverage rev #2, 21:22 CEST) · Local checks: increment diff + full file contents read; suites 294/294; 4/4 mutation probes directional (anchor membership, offset clamp, trim-aware save, History flush)

## 🔮 fufu~ Jibril reviewed your code! One fix commit, seven files, and every single item answered — the two bugs, the flush, and two honest adjudications on the ideas. I pulled `f2f6fee`, read the full increment and every changed file, ran all four suites, and mutation-probed each fix with my own hands. Care to guess what I found? ♡ ### Verdict: ✅ Looks good to me~ #### ⛔ Round 1, closed~ 1. **The focus that outlived its screen — closed twice over.** `FocusedSegmentId` is now a computed anchor (`_focused is { } id && Chapter.Segments.Any(s => s.Id == id) ? id : null`) *and* `OnParametersSet` clears `_focused` on a chapter-id change. Belt and suspenders: the property alone would catch both stale shapes even if a future code path forgot the reset. The new test walks both of my repros — focus in Ch.1 → New chapter → Scene break lands as Chapter 2's only segment, no alert; back in Ch.1, focus "Two.", another session deletes it → break appends after "One." — and my probe (membership check gutted to `_focused`) turned exactly that test red, nothing else. 2. **Enter after a trailing space — closed at the root.** `OnSplitAsync` clamps `offset` to `action.Text.TrimEnd().Length` (line 86), and — the part that made me genuinely giddy — `SaveTypedAsync` now compares the typed text *trimmed* against the stored one, so "One two. " over a stored "One two." no longer journals a phantom `edited a paragraph` before the cut. I checked the ceiling is honest: `Segment.Normalize` is exactly `TrimEnd`, so after a successful save the clamp bound is byte-identical to the stored length, and `SplitSegment` accepts offset == length (your own effect test pins the blank-tail split at offset 10 over "One two. "). Both probes red on exactly their own tests: clamp removed → 2 red; `TrimEnd` comparison reverted → 1 red with the phantom edit visible in the scopes assertion. Oh, and scarlet's reachability argument for the old 💡2 — with the clamp an out-of-range offset is no longer constructible, a scene break has no textarea, a vanished paragraph refuses the save itself — I traced all three; correct. A saved-then-refused split is now unreachable from the keyboard. 3. **History flushes (old 💡3, taken beyond the ask).** `HistoryPanel` grew a `Flush` EventCallback the page wires to the very `FlushAsync` Ctrl+Z uses; Save-a-version, Revert (in the confirm) and Redo all await it before dispatching. I mutation-probed the wiring (flush dropped from SaveVersion → exactly the two new tests red), and I traced the subtle part: after a flush-then-revert the mid-pause edit lands *inside* the revert's range — the confirm text "Every change since then is taken back" finally tells the truth. The page test's Redo arm passing while its flush journals a new active row is a fake-world property (the fakes' stores don't journal — documented on `FakeChangeJournal`), and that's fine: production's shape — a new edit above the undone rows kills the redo set — is ADR 0022's own rule and pinned by the integration suite. The adapter test pins what the adapter owns: the flush happens before the dispatch. 💡 1 (effect overlap on the scoped `DbContext`) — declined with the right reasoning: the two pairs I caught aren't special, every `DomainChangesReceived` fans out across effect classes, so a scoped gate is an app-wide change, and nothing can interleave on SQLite today. Filed in the PR body under Deferred with a named trigger. That's exactly how I'd have parked it~ #### ✅ What I liked~ - **The clamp comment** (ManuscriptEffects.cs:82–84) — "Enter at the end of `One two. ` is offset 9 in a paragraph now 8 long: the cut lands after the trimmed text, and the tail is blank either way" — teaches the whole bug in three lines. Future readers won't reintroduce it. - **The fix shapes themselves.** The offset ceiling derived from `action.Text` (what the sender typed) instead of a store round-trip, the anchor resolved against the chapter on screen instead of bookkeeping on every mutation — both close the *class*, not the instance. My yandere heart is at peace~ ♡ - **The effect-level test for the phantom edit** — asserting the journal scopes (no `edited a paragraph`) catches what the text assertions alone never would. That's the discipline I probe for, already there. - **Coverage moved the right way**: `ManuscriptColumn` 94.1→94.5% line, 91.6→93.3% branch (the new anchor arms lit up), `ManuscriptEffects`/`HistoryPanel` at 100%/100%. 294/294 locally (43 + 11 + 136 + 104 — matching your reply exactly), four mutation probes red-on-own-test, clone left pristine at `f2f6fee`. Two rounds, two real bugs found, two real bugs dead, zero scope creep in the fix. This is everything the slice promised~ Merge it before I change my mind ♡ --- *Automated review by Jibril · 2026-09-22* *CI/CD: passed for head SHA f2f6fee (build ✅ + test ✅ + coverage rev #2, 21:22 CEST) · Local checks: increment diff + full file contents read; suites 294/294; 4/4 mutation probes directional (anchor membership, offset clamp, trim-aware save, History flush)*
bjoern merged commit f90d25d8db into main 2026-09-22 21:41:24 +02:00
bjoern deleted branch feat/p1-workspace 2026-09-22 21:41:25 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Tsumugi!10
No description provided.