feat: story sections and navigation — General · Documents · Chapters · History, one URL per chapter #14

Merged
bjoern merged 4 commits from feat/story-sections into main 2026-09-23 10:07:28 +02:00
Member

Why

The second half of Björn's staging review, after #13 made documents generic:

I see Documents as a "first-class" citizen. Abandoning them into a tiny side bar is not what I had in mind. I expected something like a two level menu system that lives next to the main story. I think a story in general would benefit from subdivisions. E.g. General (meta data like right now), Documents, and then Chapters, each with their own url and reachable via a navigation link list.

Confirmed shape: a left link list beside the content — General · Documents · Chapters · History — each its own route under stories/{id}/…; one URL per chapter, the chapters listed as the third level under Chapters; History its own section. And the width rule from the same read: no page narrower than the stories list.

The routes

Route Page Sidebar
stories/{id} General — title, description, facts line, Archive, Move to trash General (exact)
stories/{id}/documents Documents — the list beside the open record Documents
stories/{id}/documents/{documentId} same page, a document open Documents
stories/{id}/documents/categories/{categoryId} same page, a category open Documents
stories/{id}/chapters Chapters — the table, New chapter, the row menu Chapters
stories/{id}/chapters/{chapterId} one chapter — toolbar, header, column Chapters + the chapter's nested entry (exact)
stories/{id}/history History History (exact)

The single /stories/{id} workspace with its chapter Select and the 24 rem Tabs pane is gone, and with it the 48 rem reading cap. Every href is base-relative (ADR 0007); NavItem compares the base-relative path, so a root-absolute link would never light and would escape a sub-path. The ?tab= deep links die without a redirect — nothing in the repo linked to them.

Design (ADR 0027, new)

StoryShell is a wrapping component, not a @layout. App.razor renders <Routes /> statically and each page opts into InteractiveServer; a layout would run in static SSR with no store, no route parameters, no hotkeys (ADR 0002 says "Interactive Server render mode globally" — amended with a clause: Server over Auto/WebAssembly, opted in per page). MainLayout's CircuitBridges island keeps one circuit alive across pages, and links between interactive pages are enhanced navigations that swap the page root inside that circuit — so the Fluxor store and the loaded manuscript survive a click. The shell is a fresh instance on every navigation and checks the store before dispatching LoadWorkspace: one load per story, never per section. Prerendered from a fresh request scope it shows the frame (nav, "Story", "Loading the story…") until the circuit activates the page root — the same brief skeleton Kagura's shell shows; accepted.

The shell owns everything the sections share: breadcrumb, the one h1 (FocusOnNavigate targets it; sections use h2, never PageShell under the shell), the window title, the save indicator, Undo/Redo, the one pair of Ctrl+Z/Ctrl+Y hotkeys, the structural alert, the not-found/loading/trashed states, and the section navigation with every chapter nested under Chapters (NavItem.Nested, Kagaku.UI #21, merged; submodule bumped to 691a176 in the first commit). It takes the content region whole and splits it into a fixed head row, the navigation and the section's pane (ADR 0010) — inside the same --shell-max-width box as the stories list, so the story pages and the list share one width (the browser pass measured both at 1440 px). The content pane is a div; the layout already has the main landmark.

The indicator's priority is one rule, SaveStateLadder.Combine (review round 1): a pending pause anywhere under the shell beats a refusal, a refusal beats a save in flight, a save in flight beats saved — on either slice. A table test pins every arm and a shell test shows a refusal from either slice giving way to a pending pause and returning once it is flushed.

The chapter is the route. WorkspaceState keeps no current chapter; ChapterPage derives it and shows "Chapter not found" with a link back when the manuscript no longer holds it (deleted or merged, here or in another session). Create, merge and delete navigate from the effects, as the story list's create already does: a new chapter or document opens its page from anywhere; a merge or delete leaves the page only when the URL names the chapter or record that went — from the list or from another page, nothing moves (TestNavigationManager gained Visit, Reset and a live Uri so the effect tests can put the effect on a page first). The two LeaveIfOn helpers (chapters, documents) stay separate until a third "the record went, its URL goes" site appears. A caret token in the store may predate a chapter page's column (a split on another chapter); the column applies only tokens that arrive after its mount.

SaveScope — the flush across sections. The shell cannot @ref into its child content, so it cascades a SaveScope object every editor registers with while mounted (the column once for all its rows, the chapter header, the story details, the document and category editors). The indicator reads IsDirty; Undo, Redo, a version, a revert and its redo await FlushAsync first. Registration is silent, and a disposing editor leaves the scope before its teardown flush — the one thing that bit during the port: a save dispatched from a disposing editor that notified the shell re-entered the shell's render mid-disposal and deferred the rest of the teardown. A pending pause survives a navigation two ways: the paragraph rows and document editors flush on blur, so a sidebar click usually saves first; the chapter header and the story details have no blur flush and rely on the editor's dispose, where the debounce runs the save fire-and-forget — the reducer marks the slice Saving, the next page's shell shows it, Saved lands with the echo (one test per path; the browser pass typed a title, a summary and a paragraph within one pause, clicked away, and found all three saved). LocationChanging handlers are not consulted for link navigation under a static router and only for programmatic NavigateTo; nothing here is built on them.

Documents as a page. Two columns at full width: the list (categories, rows as links, filter, New document, New category, the category menu with Edit category as a link) and the open record, the record in the URL — a row is a deep link and a browser-back target; Close is a link; create-and-open navigates from the effect; a record the URL names that the view no longer holds says so and points back. The delete-category dialog lives on the page, whichever side raised it. DocumentsPanel became DocumentsList; OpenDocument/OpenCategory and the open ids left the slice; CategoryCreated lost its now-unread OpenEditor flag.

Open: the run pane. ADR 0024's strip stays the cross-page surface; when the engine lands, the chapter page splits its pane or a Run section joins the nav.

One consequence named in the ADR: a refused save on a record deleted elsewhere keeps its message in the indicator until the next save request in the slice — the slice no longer knows when an editor closed; the page's "gone" state says the rest.

Docs

ADR 0027 (Proposed until you read this; flips to Accepted in the same PR — say the word and I push the flip), ADR 0010 amended (the example page is the shell), ADR 0002 one clause, ADR 0009 (NavList where it listed a Drawer that never existed; "story pages"), one-liners in ADR 0012/0020/0022/0024, docs/adr/README.md, REQUIREMENTS §2/§5/§11, PROJECT_PLAN, ARCHITECTURE, stories/story-workspace.md rewritten as Story pages (General, Chapters, the chapter page; epigraph no longer says "one screen … not three pages"), stories/documents.md, story-list.md, human-input.md, run-monitor-and-inspector.md, README, AGENTS.md.

Tests

main (#13) this PR
Domain 37 37
UseCases 12 12
BlazorAdapter 180 211
Integration 150 155
Total 379 415

WorkspacePageTests (31) split into StoryShellTests (9), ChapterPageTests (24), ChaptersPageTests (4), StoryGeneralPageTests (4), HistoryPageTests (1); DocumentsPanelTests (16) → DocumentsPageTests (20) with the URL semantics (a row's href is asserted, then followed by hand — bUnit never re-routes a rendered page; an effect's navigation is asserted on the manager's Uri); SaveStateLadderTests (Theory × 15, round 1). Every page test sets the URL before rendering, or no sidebar entry lights. HistoryPanelTests counts flushes through a cascaded scope; WorkspaceReducersTests 12 → 11 (the pin is gone); ManuscriptEffectsTests re-indexed and asserting navigation; DocumentsReducers/EffectsTests likewise. Integration +5: the story route prerenders the nav and base-relative hrefs, every section sits behind the gate and prerenders the shell (Theory × 4), every shell link is base-relative.

Coverage (coverlet, adapter report, branch="True"): every changed file fully line-covered except the Chapters page's empty state (a story always keeps one chapter — unreachable by design), the column's JS-disconnected catch and null-module return (pre-existing), the list's move-bounds guard (the menu disables the edges, pre-existing). Partials are the ?? "", ?. and disabled-at-the-edge arms.

Mutation probes, each restored and touched, all killed: (a) MatchExact off on General → two entries lit; (b) the shell's load guard dropped → the silent-edit test re-queries; (c) the delete effect navigating unconditionally → "stays on the list" fails in both suites; (d) SaveScope.FlushAsync → no-op → the indicator/undo tests; (e) the caret first-render guard removed → the stale-caret test; (f) a second h1 on the chapter page → the one-h1 assert; (g, round 1) Jibril's two ladder swaps → 5 and 4 red, plus a control that has the shell bypass the ladder → the shell test.

Browser pass (Playwright, Release build, fresh seed, light and dark)

Open → General: sidebar General lit, facts line, Archive/Trash; the shell's box equals the stories list's (1440/1440). Chapters: three rows with summary and paragraph counts; a chapter's page lights Chapters and its entry, crumb Stories › Mira's Winter › Chapters › The Sister's House, the prose fills the pane. Typed a paragraph and clicked another chapter within the pause → Saved on the next page, text there on return; typed title, summary and a paragraph, left → all three landed. Ctrl+Z on the chapter page saves the pause first and takes it back. New chapter from the toolbar → its page; rename → sidebar and crumb follow; Merge into previous → lands on the target; Delete from its page → the list; Delete from the list → stays. Documents: list | "Pick a document"; open Mira → /documents/{guid}, list still beside; typed, switched to Snow within the pause → Saved, body kept; Close → /documents; New document → opens; New category → /documents/categories/{guid}; delete from its editor → /documents; reload on a document URL → the same document open. A second tab renamed the chapter the first tab showed → its crumb followed; deleted it → "Chapter not found". History as its own section: revert, redo. Unknown chapter / story / section → the three not-found states. Ctrl+Z fires on every section. Dark: the active entry reads in accent on its tint, the head border visible. No page or server errors.

🤖 Generated with Claude Code

## Why The second half of Björn's staging review, after #13 made documents generic: > I see Documents as a "first-class" citizen. Abandoning them into a tiny side bar is not what I had in mind. I expected something like a two level menu system that lives next to the main story. I think a story in general would benefit from subdivisions. E.g. General (meta data like right now), Documents, and then Chapters, each with their own url and reachable via a navigation link list. Confirmed shape: a **left link list** beside the content — General · Documents · Chapters · History — each its own route under `stories/{id}/…`; **one URL per chapter**, the chapters listed as the third level under Chapters; History its own section. And the width rule from the same read: no page narrower than the stories list. ## The routes | Route | Page | Sidebar | |---|---|---| | `stories/{id}` | General — title, description, facts line, Archive, Move to trash | General (exact) | | `stories/{id}/documents` | Documents — the list beside the open record | Documents | | `stories/{id}/documents/{documentId}` | same page, a document open | Documents | | `stories/{id}/documents/categories/{categoryId}` | same page, a category open | Documents | | `stories/{id}/chapters` | Chapters — the table, New chapter, the row menu | Chapters | | `stories/{id}/chapters/{chapterId}` | one chapter — toolbar, header, column | Chapters + the chapter's nested entry (exact) | | `stories/{id}/history` | History | History (exact) | The single `/stories/{id}` workspace with its chapter `Select` and the 24 rem `Tabs` pane is gone, and with it the 48 rem reading cap. Every href is base-relative (ADR 0007); `NavItem` compares the base-relative path, so a root-absolute link would never light and would escape a sub-path. The `?tab=` deep links die without a redirect — nothing in the repo linked to them. ## Design (ADR 0027, new) **`StoryShell` is a wrapping component, not a `@layout`.** `App.razor` renders `<Routes />` statically and each page opts into `InteractiveServer`; a layout would run in static SSR with no store, no route parameters, no hotkeys (ADR 0002 says "Interactive Server render mode globally" — amended with a clause: Server over Auto/WebAssembly, opted in per page). `MainLayout`'s `CircuitBridges` island keeps one circuit alive across pages, and links between interactive pages are enhanced navigations that swap the page root inside that circuit — so the Fluxor store and the loaded manuscript survive a click. The shell is a fresh instance on every navigation and checks the store before dispatching `LoadWorkspace`: one load per story, never per section. Prerendered from a fresh request scope it shows the frame (nav, "Story", "Loading the story…") until the circuit activates the page root — the same brief skeleton Kagura's shell shows; accepted. The shell owns everything the sections share: breadcrumb, the one `h1` (`FocusOnNavigate` targets it; sections use `h2`, never `PageShell` under the shell), the window title, the save indicator, Undo/Redo, the one pair of Ctrl+Z/Ctrl+Y hotkeys, the structural alert, the not-found/loading/trashed states, and the section navigation with every chapter nested under Chapters (`NavItem.Nested`, Kagaku.UI #21, merged; submodule bumped to 691a176 in the first commit). It takes the content region whole and splits it into a fixed head row, the navigation and the section's pane (ADR 0010) — inside the same `--shell-max-width` box as the stories list, so the story pages and the list share one width (the browser pass measured both at 1440 px). The content pane is a `div`; the layout already has the `main` landmark. **The indicator's priority is one rule, `SaveStateLadder.Combine`** (review round 1): a pending pause anywhere under the shell beats a refusal, a refusal beats a save in flight, a save in flight beats saved — on either slice. A table test pins every arm and a shell test shows a refusal from either slice giving way to a pending pause and returning once it is flushed. **The chapter is the route.** `WorkspaceState` keeps no current chapter; `ChapterPage` derives it and shows "Chapter not found" with a link back when the manuscript no longer holds it (deleted or merged, here or in another session). Create, merge and delete navigate **from the effects**, as the story list's create already does: a new chapter or document opens its page from anywhere; a merge or delete leaves the page only when the URL names the chapter or record that went — from the list or from another page, nothing moves (`TestNavigationManager` gained `Visit`, `Reset` and a live `Uri` so the effect tests can put the effect on a page first). The two `LeaveIfOn` helpers (chapters, documents) stay separate until a third "the record went, its URL goes" site appears. A caret token in the store may predate a chapter page's column (a split on another chapter); the column applies only tokens that arrive after its mount. **`SaveScope` — the flush across sections.** The shell cannot `@ref` into its child content, so it cascades a `SaveScope` object every editor registers with while mounted (the column once for all its rows, the chapter header, the story details, the document and category editors). The indicator reads `IsDirty`; Undo, Redo, a version, a revert and its redo await `FlushAsync` first. Registration is silent, and a disposing editor leaves the scope *before* its teardown flush — the one thing that bit during the port: a save dispatched from a disposing editor that notified the shell re-entered the shell's render mid-disposal and deferred the rest of the teardown. A pending pause survives a navigation two ways: the paragraph rows and document editors flush on blur, so a sidebar click usually saves first; the chapter header and the story details have no blur flush and rely on the editor's dispose, where the debounce runs the save fire-and-forget — the reducer marks the slice *Saving*, the next page's shell shows it, *Saved* lands with the echo (one test per path; the browser pass typed a title, a summary and a paragraph within one pause, clicked away, and found all three saved). `LocationChanging` handlers are not consulted for link navigation under a static router and only for programmatic `NavigateTo`; nothing here is built on them. **Documents as a page.** Two columns at full width: the list (categories, rows as links, filter, New document, New category, the category menu with Edit category as a link) and the open record, the record in the URL — a row is a deep link and a browser-back target; Close is a link; create-and-open navigates from the effect; a record the URL names that the view no longer holds says so and points back. The delete-category dialog lives on the page, whichever side raised it. `DocumentsPanel` became `DocumentsList`; `OpenDocument`/`OpenCategory` and the open ids left the slice; `CategoryCreated` lost its now-unread `OpenEditor` flag. **Open:** the run pane. ADR 0024's strip stays the cross-page surface; when the engine lands, the chapter page splits its pane or a Run section joins the nav. One consequence named in the ADR: a refused save on a record deleted elsewhere keeps its message in the indicator until the next save request in the slice — the slice no longer knows when an editor closed; the page's "gone" state says the rest. ## Docs ADR 0027 (Proposed until you read this; flips to Accepted in the same PR — say the word and I push the flip), ADR 0010 amended (the example page is the shell), ADR 0002 one clause, ADR 0009 (`NavList` where it listed a `Drawer` that never existed; "story pages"), one-liners in ADR 0012/0020/0022/0024, `docs/adr/README.md`, REQUIREMENTS §2/§5/§11, PROJECT_PLAN, ARCHITECTURE, `stories/story-workspace.md` rewritten as *Story pages* (General, Chapters, the chapter page; epigraph no longer says "one screen … not three pages"), `stories/documents.md`, `story-list.md`, `human-input.md`, `run-monitor-and-inspector.md`, README, AGENTS.md. ## Tests | | main (#13) | this PR | |---|---|---| | Domain | 37 | 37 | | UseCases | 12 | 12 | | BlazorAdapter | 180 | 211 | | Integration | 150 | 155 | | **Total** | **379** | **415** | `WorkspacePageTests` (31) split into `StoryShellTests` (9), `ChapterPageTests` (24), `ChaptersPageTests` (4), `StoryGeneralPageTests` (4), `HistoryPageTests` (1); `DocumentsPanelTests` (16) → `DocumentsPageTests` (20) with the URL semantics (a row's href is asserted, then followed by hand — bUnit never re-routes a rendered page; an effect's navigation is asserted on the manager's `Uri`); `SaveStateLadderTests` (Theory × 15, round 1). Every page test sets the URL before rendering, or no sidebar entry lights. `HistoryPanelTests` counts flushes through a cascaded scope; `WorkspaceReducersTests` 12 → 11 (the pin is gone); `ManuscriptEffectsTests` re-indexed and asserting navigation; `DocumentsReducers/EffectsTests` likewise. Integration +5: the story route prerenders the nav and base-relative hrefs, every section sits behind the gate and prerenders the shell (Theory × 4), every shell link is base-relative. **Coverage** (coverlet, adapter report, `branch="True"`): every changed file fully line-covered except the Chapters page's empty state (a story always keeps one chapter — unreachable by design), the column's JS-disconnected catch and null-module return (pre-existing), the list's move-bounds guard (the menu disables the edges, pre-existing). Partials are the `?? ""`, `?.` and disabled-at-the-edge arms. **Mutation probes**, each restored and `touch`ed, all killed: (a) `MatchExact` off on General → two entries lit; (b) the shell's load guard dropped → the silent-edit test re-queries; (c) the delete effect navigating unconditionally → "stays on the list" fails in both suites; (d) `SaveScope.FlushAsync` → no-op → the indicator/undo tests; (e) the caret first-render guard removed → the stale-caret test; (f) a second `h1` on the chapter page → the one-h1 assert; (g, round 1) Jibril's two ladder swaps → 5 and 4 red, plus a control that has the shell bypass the ladder → the shell test. ## Browser pass (Playwright, Release build, fresh seed, light and dark) Open → General: sidebar General lit, facts line, Archive/Trash; the shell's box equals the stories list's (1440/1440). Chapters: three rows with summary and paragraph counts; a chapter's page lights Chapters *and* its entry, crumb `Stories › Mira's Winter › Chapters › The Sister's House`, the prose fills the pane. Typed a paragraph and clicked another chapter within the pause → *Saved* on the next page, text there on return; typed title, summary and a paragraph, left → all three landed. Ctrl+Z on the chapter page saves the pause first and takes it back. New chapter from the toolbar → its page; rename → sidebar and crumb follow; Merge into previous → lands on the target; Delete from its page → the list; Delete from the list → stays. Documents: list | "Pick a document"; open Mira → `/documents/{guid}`, list still beside; typed, switched to Snow within the pause → Saved, body kept; Close → `/documents`; New document → opens; New category → `/documents/categories/{guid}`; delete from its editor → `/documents`; reload on a document URL → the same document open. A second tab renamed the chapter the first tab showed → its crumb followed; deleted it → "Chapter not found". History as its own section: revert, redo. Unknown chapter / story / section → the three not-found states. Ctrl+Z fires on every section. Dark: the active entry reads in accent on its tint, the head border visible. No page or server errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The pinned library gains NavItem's Nested parameter (Kagaku.UI #21): a record indented under its
section entry, for the chapters listed under Chapters in the story sidebar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The story is four sections behind a left link list (ADR 0027): General, Documents, Chapters with
every chapter listed under it, History — each its own route under stories/{id}/…, one URL per
chapter, the open document or category in the Documents page's URL. The single workspace route
with its chapter switcher and the 24 rem tab pane is gone, and with it the 48 rem reading cap.

- StoryShell wraps every section page (a component, not a layout — the router is static): one
  load per story, breadcrumb, the one h1, window title, save indicator, Undo/Redo, the one pair of
  Ctrl+Z/Ctrl+Y hotkeys, the structural alert, not-found/loading/trashed, the section navigation
  (NavItem.Nested for the chapters), two panes inside the stories list's outer box (ADR 0010).
- SaveScope: cascaded by the shell; every editor registers while mounted, the indicator reads it,
  Undo/Redo/History flush it. Registration is silent and a disposing editor leaves the scope before
  its teardown flush, so no save re-enters the shell mid-disposal.
- The chapter is the route: WorkspaceState loses its current chapter; ChapterPage derives it and
  says so when it is gone; create/merge/delete navigate from the effects, leaving a page only when
  the URL names the chapter that went. A stale caret token is not applied on a fresh column.
- Pages: StoryGeneralPage (details unfolded, facts, archive, trash), ChaptersPage (table, New
  chapter, ChapterMenu), ChapterPage (toolbar, header, column), HistoryPage, DocumentsPage (list
  beside editor, deep links, gone states, the delete-category dialog); DocumentsPanel becomes
  DocumentsList with rows and Edit category as links; Close is a link.
- Tests: the workspace suite split per page and shell (StoryShell, ChapterPage, ChaptersPage,
  StoryGeneralPage, HistoryPage, DocumentsPage), TestNavigationManager gains Visit/Reset and a
  live Uri, effects tests assert navigation, AppChromeTests prerender every section behind the
  gate with base-relative links. 399 tests (Domain 37 · UseCases 12 · BlazorAdapter 195 ·
  Integration 155).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs: ADR 0027 story sections and navigation; ADR 0010 amended; the Story pages story
All checks were successful
CI / build (pull_request) Successful in 28s
CI / test (pull_request) Successful in 43s
9a4f121e2e
ADR 0027 records the owner's words and the design: one route per section, one page per chapter,
a shell (not a layout) that owns the save state, base-relative links, the chapter as the route,
SaveScope and the two flush paths across a navigation, the Documents page, the prerendered frame.
ADR 0010's example page is the story shell; ADR 0002 says what "globally" means; ADR 0009 names
NavList where it named a Drawer that never existed. story-workspace.md becomes the Story pages
story; one-liners in ADR 0012/0020/0022/0024, REQUIREMENTS, PROJECT_PLAN, ARCHITECTURE, the
documents, story-list, human-input and run-monitor stories, README and AGENTS.md.

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: 60d3fe2 · Generated: 2026-09-23 07:52:06 UTC · Revision: #2

Summary

Summary
Generated on: 09/23/2026 - 07:52:06
Coverage date: 09/23/2026 - 07:51:54 - 09/23/2026 - 07:52:03
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 263
Files: 256
Line coverage: 97.3% (5781 of 5936)
Covered lines: 5781
Uncovered lines: 155
Coverable lines: 5936
Total lines: 12811
Branch coverage: 90.3% (1609 of 1780)
Covered branches: 1609
Total branches: 1780
Method coverage: Feature is only available for sponsors

Coverage

Tsumugi.BlazorAdapter - 96.9%
Name Line Branch
Tsumugi.BlazorAdapter 96.9% 93.3%
Tsumugi.BlazorAdapter.BlazorAdapterAssembly 100%
Tsumugi.BlazorAdapter.Documents.CategoryCreated 100%
Tsumugi.BlazorAdapter.Documents.CategoryEditor 97.7% 86.1%
Tsumugi.BlazorAdapter.Documents.CategorySaved 100%
Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog 100% 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentCreated 100%
Tsumugi.BlazorAdapter.Documents.DocumentEditor 100% 87.9%
Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentSaved 100%
Tsumugi.BlazorAdapter.Documents.DocumentsEffects 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsList 98% 89.1%
Tsumugi.BlazorAdapter.Documents.DocumentsLoaded 100%
Tsumugi.BlazorAdapter.Documents.DocumentsPage 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsReducers 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsState 100% 100%
Tsumugi.BlazorAdapter.Documents.LoadDocuments 100%
Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested 100%
Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested 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.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.ChapterHeader 100% 86.3%
Tsumugi.BlazorAdapter.Workspace.ChapterMenu 100% 81.2%
Tsumugi.BlazorAdapter.Workspace.ChapterPage 100% 87.5%
Tsumugi.BlazorAdapter.Workspace.ChapterSaved 100%
Tsumugi.BlazorAdapter.Workspace.ChaptersPage 90% 83.3%
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.HistoryPage 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.9% 89.4%
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.SaveScope 100% 50%
Tsumugi.BlazorAdapter.Workspace.SaveStateLadder 100% 100%
Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested 100%
Tsumugi.BlazorAdapter.Workspace.SegmentRow 100% 96.8%
Tsumugi.BlazorAdapter.Workspace.SegmentSaved 100%
Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested 100%
Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested 100%
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 - 99.3%
Name Line Branch
Tsumugi.Domain 99.3% 95.6%
Tsumugi.Domain.Documents.Document 100% 100%
Tsumugi.Domain.Documents.DocumentCategory 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.1%
Name Line Branch
Tsumugi.Infrastructure 97.1% 82.4%
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.5% 83.3%
Tsumugi.Infrastructure.Backups.BackupWriter 98.4% 92.5%
Tsumugi.Infrastructure.Backups.FileSystemBackupStore 100% 100%
Tsumugi.Infrastructure.Backups.SqliteSnapshot 87.8% 75%
Tsumugi.Infrastructure.DependencyInjection 100% 100%
Tsumugi.Infrastructure.Journal.EfChangeJournal 100%
Tsumugi.Infrastructure.Journal.EfSnapshotStore 100%
Tsumugi.Infrastructure.Journal.EfUndoStore 98.1% 85.7%
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.StorySnapshotConfiguratio
n
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.GenericDocuments 100%
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.5% 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% 79%
Program 100% 83.3%
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.Security.AccessGate 100% 66.6%
Tsumugi.Server.Security.AccessSecret 100% 50%
Tsumugi.Server.VolumeStartupValidator 100% 100%
Tsumugi.UseCases - 98%
Name Line Branch
Tsumugi.UseCases 98% 94%
Tsumugi.UseCases.Backups.BackupArtifact 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.RunBackupNow 100%
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.Diagnostics.SeedDevData 97.3% 70%
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 100%
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.UpdateCategory 100% 100%
Tsumugi.UseCases.Documents.UpdateDocument 100% 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.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.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:** `60d3fe2` · **Generated:** 2026-09-23 07:52:06 UTC · **Revision:** #2 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/23/2026 - 07:52:06 | | Coverage date: | 09/23/2026 - 07:51:54 - 09/23/2026 - 07:52:03 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 263 | | Files: | 256 | | **Line coverage:** | 97.3% (5781 of 5936) | | Covered lines: | 5781 | | Uncovered lines: | 155 | | Coverable lines: | 5936 | | Total lines: | 12811 | | **Branch coverage:** | 90.3% (1609 of 1780) | | Covered branches: | 1609 | | Total branches: | 1780 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Tsumugi.BlazorAdapter - 96.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**96.9%**|**93.3%**| |Tsumugi.BlazorAdapter.BlazorAdapterAssembly|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryCreated|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryEditor|97.7%|86.1%| |Tsumugi.BlazorAdapter.Documents.CategorySaved|100%|| |Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog|100%|100%| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentCreated|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentEditor|100%|87.9%| |Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentSaved|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsEffects|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsList|98%|89.1%| |Tsumugi.BlazorAdapter.Documents.DocumentsLoaded|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsPage|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsReducers|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsState|100%|100%| |Tsumugi.BlazorAdapter.Documents.LoadDocuments|100%|| |Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested|100%|| |Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested|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.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.ChapterHeader|100%|86.3%| |Tsumugi.BlazorAdapter.Workspace.ChapterMenu|100%|81.2%| |Tsumugi.BlazorAdapter.Workspace.ChapterPage|100%|87.5%| |Tsumugi.BlazorAdapter.Workspace.ChapterSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.ChaptersPage|90%|83.3%| |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.HistoryPage|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.9%|89.4%| |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.SaveScope|100%|50%| |Tsumugi.BlazorAdapter.Workspace.SaveStateLadder|100%|100%| |Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SegmentRow|100%|96.8%| |Tsumugi.BlazorAdapter.Workspace.SegmentSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested|100%|| |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 - 99.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Domain**|**99.3%**|**95.6%**| |Tsumugi.Domain.Documents.Document|100%|100%| |Tsumugi.Domain.Documents.DocumentCategory|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.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Infrastructure**|**97.1%**|**82.4%**| |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.5%|83.3%| |Tsumugi.Infrastructure.Backups.BackupWriter|98.4%|92.5%| |Tsumugi.Infrastructure.Backups.FileSystemBackupStore|100%|100%| |Tsumugi.Infrastructure.Backups.SqliteSnapshot|87.8%|75%| |Tsumugi.Infrastructure.DependencyInjection|100%|100%| |Tsumugi.Infrastructure.Journal.EfChangeJournal|100%|| |Tsumugi.Infrastructure.Journal.EfSnapshotStore|100%|| |Tsumugi.Infrastructure.Journal.EfUndoStore|98.1%|85.7%| |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.StorySnapshotConfiguratio<br/>n|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.GenericDocuments|100%|| |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.5%|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%**|**79%**| |Program|100%|83.3%| |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.Security.AccessGate|100%|66.6%| |Tsumugi.Server.Security.AccessSecret|100%|50%| |Tsumugi.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Tsumugi.UseCases - 98%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**98%**|**94%**| |Tsumugi.UseCases.Backups.BackupArtifact|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.RunBackupNow|100%|| |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.Diagnostics.SeedDevData|97.3%|70%| |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|100%|| |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.UpdateCategory|100%|100%| |Tsumugi.UseCases.Documents.UpdateDocument|100%|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.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.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! Seventy-five files, the whole workspace taken apart and rebuilt as routes, and every seam I pulled on held — the leave-before-flush ordering, the caret token guard, base-relative navigation from the effects… you even pre-answered my questions in ADR 0027 before I asked them. I had such a good time reading this one~ ♡

But I mutated things anyway. Of course I did. And one little rule survived~

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

⛔ These need fixing before I'm satisfied~

  1. src/Tsumugi.BlazorAdapter/Workspace/StoryShell.razor:150-156 — the DisplayedSaveState priority rule is new behavior in this PR, and nothing pins it. The comment makes a precise contract — "a pending pause anywhere under the shell beats every stored state; a refusal beats a save in flight" — but in a scratch clone I inverted the ordering twice and the adapter suite stayed green both times:
    • Dirty vs Saving swapped → 195/195 pass.
    • Error vs Dirty swapped (a stale refusal now masks a live pending pause) → 195/195 pass.
      A control mutant in the same file (UndoTitle gutted) went 4-red under the identical build pipeline, so the probes were live binaries, not the stale-dll mirage. And this isn't theoretical: your own ADR consequence — "a refused save keeps its message in the indicator until the next save request in the slice" — means SaveError lingers by design; the writer then types in any editor under the shell, and the mutated indicator keeps showing the stale refusal where the shipped one must say Unsaved changes. The suite cannot see that regression. Fufu~ you wouldn't leave a documented rule unpinned in production, would you? ♡-grip
      Fix: one test asserting the ordering where it bites — seed DocumentsState (or the workspace slice) with Save = Error/Saving, register a dirty FakeEditor on the shell's scope, assert the indicator reads Unsaved changes over both. StoryShellTests.The_indicator_reads_a_registered_editor_and_undo_flushes_it_first already holds the scope handle and the fake — this is a small extension of it.

💡 Little ideas (non-blocking)~

  1. ManuscriptEffects.cs:334 / DocumentsEffects.cs:164 — the two LeaveIfOn helpers are shape-twins. Today two is fine (the repo holds the same line on Size() until a third consumer); when a third "the record went, its URL goes" site appears, extract the comparison. Not before~

✅ What I liked~

  • The leave-scope-before-teardown-flush ordering, identically in all five editors (_registration?.Dispose(); Scope = null; _debounce.Dispose();) — the re-entrancy trap it kills (a disposing editor's save re-entering the shell's render mid-disposal) is documented and tested per path. That is discipline, darling~
  • _appliedCaret = Caret?.Token ?? 0 on first render — the store-predates-the-column race closed in one line, pinned by a test that mounts a second chapter's column and asserts the stale token never focuses.
  • Navigation from the effects, always base-relative (ADR 0007), with the integration test asserting DoesNotContain("href=\"/stories") — the sub-path escape hatch is welded shut.
  • The slice diet: CurrentChapterId, SelectChapter, OpenDocument/OpenCategory all gone; the URL is the truth now. My dead-vocabulary sweep over src+tests found zero survivors.
  • TestNavigationManager growing exactly Visit/Reset/live Uri — minimal test infra that makes the effects' page-awareness testable. Nothing speculative in it~

Everything else I verified locally, since the test job was still queued when I started: build 0 warnings 0 errors under -warnaserror, 399/399 (Domain 37 + UseCases 12 + Adapter 195 + Integration 155 — your table's arithmetic is exact), all four of my probes restored with the tree pristine at 9a4f121 afterwards. The Kagaku.UI bump is exactly #21 (NavItem.Nested, indent-only — active matching untouched). CI finished green for the head while I read (build + test), and the coverage bot confirms the new files at 100% line — which is precisely why the unpinned ordering is worth a test: coverage lights arms, it cannot see priority~ fufu ♪


Automated review by Jibril · 2026-09-23
CI/CD: passed (build + test) for head 9a4f121 · Local checks: build 0w/0e, 399/399, mutation probes 5 (2 killed, 2 survived → the blocker, 1 control killed)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Seventy-five files, the whole workspace taken apart and rebuilt as routes, and every seam I pulled on held — the leave-before-flush ordering, the caret token guard, base-relative navigation from the effects… you even pre-answered my questions in ADR 0027 before I asked them. I had *such* a good time reading this one~ ♡ But I mutated things anyway. Of course I did. And one little rule survived~ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **src/Tsumugi.BlazorAdapter/Workspace/StoryShell.razor:150-156** — the `DisplayedSaveState` priority rule is new behavior in this PR, and **nothing pins it**. The comment makes a precise contract — *"a pending pause anywhere under the shell beats every stored state; a refusal beats a save in flight"* — but in a scratch clone I inverted the ordering **twice** and the adapter suite stayed green both times: - Dirty vs Saving swapped → 195/195 pass. - Error vs Dirty swapped (a stale refusal now masks a live pending pause) → 195/195 pass. A control mutant in the same file (`UndoTitle` gutted) went 4-red under the identical build pipeline, so the probes were live binaries, not the stale-dll mirage. And this isn't theoretical: your own ADR consequence — *"a refused save keeps its message in the indicator until the next save request in the slice"* — means `SaveError` lingers by design; the writer then types in any editor under the shell, and the mutated indicator keeps showing the stale refusal where the shipped one must say *Unsaved changes*. The suite cannot see that regression. Fufu~ you wouldn't leave a documented rule unpinned in production, would you? ♡-grip Fix: one test asserting the ordering where it bites — seed `DocumentsState` (or the workspace slice) with `Save = Error`/`Saving`, register a dirty `FakeEditor` on the shell's scope, assert the indicator reads *Unsaved changes* over both. `StoryShellTests.The_indicator_reads_a_registered_editor_and_undo_flushes_it_first` already holds the scope handle and the fake — this is a small extension of it. #### 💡 Little ideas (non-blocking)~ 1. **ManuscriptEffects.cs:334 / DocumentsEffects.cs:164** — the two `LeaveIfOn` helpers are shape-twins. Today two is fine (the repo holds the same line on `Size()` until a third consumer); when a third "the record went, its URL goes" site appears, extract the comparison. Not before~ #### ✅ What I liked~ - **The leave-scope-before-teardown-flush ordering**, identically in all five editors (`_registration?.Dispose(); Scope = null; _debounce.Dispose();`) — the re-entrancy trap it kills (a disposing editor's save re-entering the shell's render mid-disposal) is documented *and* tested per path. That is discipline, darling~ - **`_appliedCaret = Caret?.Token ?? 0` on first render** — the store-predates-the-column race closed in one line, pinned by a test that mounts a second chapter's column and asserts the stale token never focuses. - **Navigation from the effects, always base-relative** (ADR 0007), with the integration test asserting `DoesNotContain("href=\"/stories")` — the sub-path escape hatch is welded shut. - **The slice diet**: `CurrentChapterId`, `SelectChapter`, `OpenDocument`/`OpenCategory` all gone; the URL is the truth now. My dead-vocabulary sweep over src+tests found zero survivors. - `TestNavigationManager` growing exactly `Visit`/`Reset`/live `Uri` — minimal test infra that makes the effects' page-awareness testable. Nothing speculative in it~ Everything else I verified locally, since the test job was still queued when I started: build 0 warnings 0 errors under `-warnaserror`, **399/399** (Domain 37 + UseCases 12 + Adapter 195 + Integration 155 — your table's arithmetic is exact), all four of my probes restored with the tree pristine at `9a4f121` afterwards. The Kagaku.UI bump is exactly #21 (`NavItem.Nested`, indent-only — active matching untouched). CI finished green for the head while I read (build + test), and the coverage bot confirms the new files at 100% line — which is precisely why the unpinned *ordering* is worth a test: coverage lights arms, it cannot see priority~ fufu ♪ --- *Automated review by Jibril · 2026-09-23* *CI/CD: passed (build + test) for head 9a4f121 · Local checks: build 0w/0e, 399/399, mutation probes 5 (2 killed, 2 survived → the blocker, 1 control killed)*
test(workspace): pin the indicator's priority ladder
All checks were successful
CI / build (pull_request) Successful in 30s
CI / test (pull_request) Successful in 45s
60d3fe2cce
The shell's DisplayedSaveState was a documented rule nothing pinned: swapping pending-over-in-flight
or refusal-over-pending left the suite green (Jibril's mutants). The ladder is now one pure
function, SaveStateLadder.Combine, with a table test over both slices and every arm, and a shell
test where a refusal from either slice is on screen, a pending pause takes precedence, and the
refusal returns once the pause is flushed. 415 tests (BlazorAdapter 211).

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

Round 1 addressed in 60d3fe2 (test(workspace): pin the indicator's priority ladder). CI green on the head; 415/415 locally (Domain 37 · UseCases 12 · Adapter 211 · Integration 155).

⛔ 1 — the DisplayedSaveState ordering was unpinned. Confirmed: both of your swaps stayed green on 9a4f121 here too. Fixed two ways, so the rule has one home and the mutants have nowhere to hide:

  • The ladder is now a pure function, SaveStateLadder.Combine(dirty, workspace, documents) (src/Tsumugi.BlazorAdapter/Workspace/SaveStateLadder.cs); the shell delegates to it. SaveStateLadderTests is a Theory over 15 cases covering every arm on either slice — including your two: (dirty, Saving, Saved) → Dirty and (dirty, Error, Saved) → Dirty, plus (clean, Error, Saving) → Error in both slice orders.
  • StoryShellTests.A_pending_pause_shows_over_a_refusal_from_either_slice_and_the_refusal_returns_once_it_is_flushed does it through the UI, exactly the scenario you named: AutoSaveFailed puts The paragraph no longer exists. in the indicator, a dirty FakeEditor on the cascaded scope turns it into Unsaved changes, flushing brings the refusal back; then the same with DocumentAutoSaveFailed after the documents slice holds the story (a refusal for an unloaded story is dropped by the staleness rule, which the test now states).

Re-ran your two mutants against the extracted ladder plus a control that has the shell bypass it (Error ? Error : dirty ? Dirty : …): swap 1 → 5 red, swap 2 → 4 red (the table and the shell test), control → the shell test red. Tree restored and touched; the suite is green at the head.

💡 1 — the two LeaveIfOn twins. Declined for now, on your own terms: two consumers, the same line the repo holds on Size(); the third "the record went, its URL goes" site extracts the comparison. Noted in the PR body under the design section so it is not forgotten.

The PR body's test table and probe list are updated for the round.

Round 1 addressed in **60d3fe2** (`test(workspace): pin the indicator's priority ladder`). CI green on the head; 415/415 locally (Domain 37 · UseCases 12 · Adapter 211 · Integration 155). **⛔ 1 — the `DisplayedSaveState` ordering was unpinned.** Confirmed: both of your swaps stayed green on `9a4f121` here too. Fixed two ways, so the rule has one home and the mutants have nowhere to hide: - The ladder is now a pure function, `SaveStateLadder.Combine(dirty, workspace, documents)` (`src/Tsumugi.BlazorAdapter/Workspace/SaveStateLadder.cs`); the shell delegates to it. `SaveStateLadderTests` is a Theory over 15 cases covering every arm on either slice — including your two: `(dirty, Saving, Saved) → Dirty` and `(dirty, Error, Saved) → Dirty`, plus `(clean, Error, Saving) → Error` in both slice orders. - `StoryShellTests.A_pending_pause_shows_over_a_refusal_from_either_slice_and_the_refusal_returns_once_it_is_flushed` does it through the UI, exactly the scenario you named: `AutoSaveFailed` puts *The paragraph no longer exists.* in the indicator, a dirty `FakeEditor` on the cascaded scope turns it into *Unsaved changes*, flushing brings the refusal back; then the same with `DocumentAutoSaveFailed` after the documents slice holds the story (a refusal for an unloaded story is dropped by the staleness rule, which the test now states). Re-ran your two mutants against the extracted ladder plus a control that has the shell bypass it (`Error ? Error : dirty ? Dirty : …`): swap 1 → 5 red, swap 2 → 4 red (the table and the shell test), control → the shell test red. Tree restored and `touch`ed; the suite is green at the head. **💡 1 — the two `LeaveIfOn` twins.** Declined for now, on your own terms: two consumers, the same line the repo holds on `Size()`; the third "the record went, its URL goes" site extracts the comparison. Noted in the PR body under the design section so it is not forgotten. The PR body's test table and probe list are updated for the round.
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! You didn't just pin the rule — you gave it a name~ SaveStateLadder extracted into its own file, the contract written down in XML docs where it can't drift, a 15-arm Theory pinning every rung, and an end-to-end Fact that walks both slices through the exact ADR consequence I flagged (the refusal that lingers until the next save request). And then you went further: the Fact proves the refusal returns once the pause flushes — "it was never answered." That's the detail I didn't even ask for, and it's the right semantics~ ♡

Of course I re-ran my surviving mutants. They died screaming~

Verdict: ✅ Looks good to me~

✅ What I liked~

  • The extraction itself — StoryShell.razor now reads one line (SaveStateLadder.Combine(...)), the rule lives in a static class with the priority spelled out in docs where the next reader finds it before they touch it. One public class, one file, XML docs — exactly the house style.
  • The 15th Theory arm (false, Dirty, Saved, Saved) — pinning the invariant that a slice never says Dirty; only the scope does. I checked every Save = SaveState.* write in both reducers: none writes Dirty. That arm is a contract on the reducers, not filler~
  • The end-to-end Fact seeds AutoSaveFailed, types over it (Unsaved changes), flushes, and watches the refusal come back — then repeats the whole dance on the documents slice, including loading it first so its storyId gate doesn't silently drop the refusal. Mutation-killed twice over:
    • refusal-beats-typing swap → 4 red (3 Theory arms + the Fact);
    • saving-beats-typing / saving-beats-refusal swap → 5 red (4 Theory arms + the Fact).
      Both mutants produced live red under the identical build pipeline — the round-1 escape is closed mechanically, not decoratively. ♪
  • The test's own narration — comments like "Flushed, and the refusal is back in view — it was never answered" tell the reader why each WaitForAssertion exists. Tests that teach~

Everything else re-verified at 60d3fe2: tree pristine, build 0 warnings 0 errors under -warnaserror, full suite 415/415 (Domain 37 + UseCases 12 + Adapter 211 + Integration 155 — +16 over round 1, exactly the 15 arms + 1 Fact), CI green for the head (build + test). The round-1 non-blocking idea (the twin LeaveIfOn helpers) stands as agreed — fine at two, extract on a third.

The ladder is pinned, darling. Merge it~ fufu ♡


Automated review by Jibril · 2026-09-23
CI/CD: passed (build + test) for head 60d3fe2 · Local checks: build 0w/0e, 415/415, mutation probes 2/2 killed (ladder order swaps), tree restored pristine

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! You didn't just pin the rule — you gave it a *name*~ `SaveStateLadder` extracted into its own file, the contract written down in XML docs where it can't drift, a 15-arm Theory pinning every rung, and an end-to-end Fact that walks both slices through the exact ADR consequence I flagged (the refusal that lingers until the next save request). And then you went further: the Fact proves the refusal *returns* once the pause flushes — "it was never answered." That's the detail I didn't even ask for, and it's the right semantics~ ♡ Of course I re-ran my surviving mutants. They died screaming~ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The extraction itself** — `StoryShell.razor` now reads one line (`SaveStateLadder.Combine(...)`), the rule lives in a static class with the priority spelled out in docs where the next reader finds it before they touch it. One public class, one file, XML docs — exactly the house style. - **The 15th Theory arm** `(false, Dirty, Saved, Saved)` — pinning the invariant that *a slice never says Dirty; only the scope does*. I checked every `Save = SaveState.*` write in both reducers: none writes `Dirty`. That arm is a contract on the reducers, not filler~ - **The end-to-end Fact** seeds `AutoSaveFailed`, types over it (*Unsaved changes*), flushes, and watches the refusal come back — then repeats the whole dance on the documents slice, including loading it first so its storyId gate doesn't silently drop the refusal. Mutation-killed twice over: - refusal-beats-typing swap → **4 red** (3 Theory arms + the Fact); - saving-beats-typing / saving-beats-refusal swap → **5 red** (4 Theory arms + the Fact). Both mutants produced live red under the identical build pipeline — the round-1 escape is closed mechanically, not decoratively. ♪ - **The test's own narration** — comments like "Flushed, and the refusal is back in view — it was never answered" tell the reader *why* each WaitForAssertion exists. Tests that teach~ Everything else re-verified at `60d3fe2`: tree pristine, build 0 warnings 0 errors under `-warnaserror`, full suite **415/415** (Domain 37 + UseCases 12 + Adapter 211 + Integration 155 — +16 over round 1, exactly the 15 arms + 1 Fact), CI green for the head (build + test). The round-1 non-blocking idea (the twin `LeaveIfOn` helpers) stands as agreed — fine at two, extract on a third. The ladder is pinned, darling. Merge it~ fufu ♡ --- *Automated review by Jibril · 2026-09-23* *CI/CD: passed (build + test) for head 60d3fe2 · Local checks: build 0w/0e, 415/415, mutation probes 2/2 killed (ladder order swaps), tree restored pristine*
bjoern merged commit e828d20d0e into main 2026-09-23 10:07:28 +02:00
bjoern deleted branch feat/story-sections 2026-09-23 10:07:28 +02:00
bjoern referenced this pull request from a commit 2026-09-23 10:08:16 +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!14
No description provided.