feat: Phase 1 · 5/7 — the story list, the trash and the settings page #9

Merged
bjoern merged 2 commits from feat/p1-story-list into main 2026-09-22 19:45:01 +02:00
Member

Fifth slice of Phase 1, in two commits: the first unjournaled rows (settings persistence — ADR 0006, 0022), then the landing page, the trash and the settings tabs over the use cases from #8. First real Fluxor feature slices (ADR 0011); every screen composed from Kagaku.UI (ADR 0009).

What's in

Settings persistence (8437321) — Kagura's key/value table: AppSetting : IUnjournaled behind IAppSettingsStore (get/set, has-secret/get-secret/set-secret, remove); EfAppSettingsStore encrypts secrets with the host's DataProtection keyring under the purpose Tsumugi.AppSettings; migration AddAppSettings (has-pending-model-changes clean). Use cases: GetProviderSettings (presence only — the view has no member that could hold the key), SaveOpenRouterKey (blank refused, trimmed, encrypted), RemoveOpenRouterKey (removing nothing is an answer, not an error), GetLlmDefaults (built-ins field by field when unset or hand-edited out of range — the engine never starts with a limit of zero) and SaveLlmDefaults (whole record: model id, effort, concurrency 1–16, safety margin 0–50 %). ReasoningEffort carries OpenRouter's own spelling (ReasoningEfforts.ToSetting/Parse), so the gateway needs no table. Built-ins: no model, model-default effort, concurrency 3 (ADR 0016), margin 5 % (ADR 0017).

The story list (/, replaces the Phase 0 placeholder) — src/Tsumugi.BlazorAdapter/Stories/: StoriesState (both lists, Filtered as a pure projection of the trimmed case-insensitive title filter × the archived toggle, HasArchived), one action record per file, StoriesReducers, StoriesEffects (three ListStories partitions merged newest-first so the toggle is a projection and not a round trip; every mutation reloads on success and dispatches StoryActionFailed on refusal; DomainChangesReceived re-queries on Story and on Chapter — the list counts chapters, so a chapter added in another tab refreshes it). StoriesPage: PageShell · DebouncedSearchField (the app clock drives the debounce, so tests advance a fake one) · ToggleButtonGroup Active / With archived · Table with title + description, chapters, modified (RelativeTime), Cost and Graph as “—” (the story's two Phase 2 columns, never a made-up zero) · a Menu per row: Archive/Unarchive (no confirmation — reversible), Move to trash (ConfirmDialog, danger) · Modal create form mounted on demand (title required, description optional; closes itself once the request settles without a refusal, stays open with the message under the field otherwise) · EmptyState for no stories / no matches (the message says when archived stories are hidden) · InlineAlert for a refused row action.

The trash (/trash) — StoryTrashPage over the same feature state: title, chapters, deleted-when, Restore (one click) and Purge behind its own ConfirmDialog — the one irreversible action, offered only here. Breadcrumb Stories › Trash.

The settings page — Tabs Param="tab": Provider (ProviderSection + its slice: whether a key is set, Remove key behind a confirmation; the section says plainly that entering a key arrives with the gateway), Defaults (DefaultsSection + its slice: model id, effort Select, concurrency and margin as number fields; typed fields auto-save on Kagaku.UI's Debounce, the pick saves at once, SaveIndicator is the only feedback, out-of-range values are refused under the field and keep the record visibly unsaved, a pending edit is flushed when the tab is left), Appearance (the browser-local ThemeSwitcher, as before). Both settings effects carry the comment that they handle no doorbell and register no EntityKinds on purpose: settings are unjournaled and never announced (ADR 0022).

StoryListItem.IsArchived joins the DTO so the adapter never spells a Domain enum. AGENTS.md's status paragraph, the story files' checkboxes (filter/toggle/trash, archive, delete→trash+restore+purge, theme) updated.

Deferred, and why

  • Open / the post-create navigation — the workspace owns the story route (slice 6); a link to nothing would be a broken href (ADR 0007).
  • Run cost to date, the active graph's name, the bundled Continue graph on create — the ledger and the graph library are Phase 2; the cells show “—” until then.
  • Entering the key, Validate & save, label and remaining credit — the story's acceptance criterion is “an invalid key is refused with the provider's message and nothing is stored”, which needs the gateway's /key check (ADR 0018). An unchecked save would store a typo the writer only discovers at run time, so the Provider tab shows state and Remove only; SaveOpenRouterKey exists and is proven, and gains the check in Phase 2.
  • Model and effort pickers over the catalog — the catalog is the gateway's (ADR 0018); the model is a plain id field until then, and the hint says so.
  • Backups and Transcripts tabs — backups are their own Phase 1 slice; transcripts are Phase 2 (ADR 0024).
  • PROJECT_PLAN.md lists the settings under Phase 2; this slice pulls the persistence and the two tabs forward. No ADR forbids it.

Tests

project main (5ed220d) this PR
Domain 40 43 (+3: AppSetting create/guards/update)
UseCases 4 11 (+7: ReasoningEfforts spelling, round trip, four unrecognised inputs, case + order; DI validation gains IAppSettingsStore)
BlazorAdapter 15 70 (+55)
Integration 84 101 (+17)
module (Vitest) 14 14

BlazorAdapter (+55) — over the real Fluxor pipeline with in-memory fakes (FakeStoryStore, FakeChapterStore, FakeAppSettingsStore, RecordingOperationContext, FixedState, a FakeTimeProvider for every debounce):

  • StoriesPageTests (14): newest-first rows with description and chapter count, the six headers and the four “—” cells, base-relative trash href; the empty state; filter narrows after the 250 ms debounce and a non-match names the filter; archived hidden → toggle shows them badged and aria-pressed follows → back; a filter only archived stories would match says they are hidden; all stories archived → “No matches … Archived stories are hidden.”; create persists title + trimmed description, lists it, closes the form, seeds Chapter 1, and the use case opened exactly one User scope created 'Ashfall'; a refused create keeps the form open with the message under the title; Cancel and the dialog's own close (onclose) both drop the form and the reopened form is blank; a blank title never reaches the store even when the disabled button is pressed; archive/unarchive from the menu with no modal and the status flipping in the store; move-to-trash asks, Cancel keeps; confirm removes as the journaled moved the story to the trash operation; a refused row action shows in the alert and the list stays; a second page in the same circuit reuses the loaded list (list reads counted).
  • StoryTrashPageTests (6): only trashed rows with their deleted-when; empty state with a way back; Restore without asking; Purge asks, Cancel keeps; confirm removes the story and its chapters from the store; a refused restore shows in the alert.
  • StoriesEffectsTests (8): load merges active + archived newest-first with the trash beside them and counts chapters; create success dispatches StoryCreated then LoadStories, blank dispatches the refusal only; each of the four row actions reloads on success — and the recorded scopes are exactly the use cases' six labels, none for the purge; each of the four reports a refusal for a missing story, and purge refuses a living one; a Story or Chapter change reloads a loaded list; nothing reloads before the first load; other kinds are ignored.
  • StoriesReducersTests (5): load/loaded, trimmed case-insensitive filter, the four toggle × filter combinations and HasArchived, the create lifecycle, every row request nulling the previous refusal.
  • SettingsPageTests (3): the three tabs, Provider as the landing, ?tab=defaults and ?tab=appearance mounting only their panel.
  • ProviderSectionTests (4): no key → says so, no Remove, names the gateway; key → “A key is set”, never the value; Remove asks, Cancel keeps; confirm forgets it.
  • ProviderSettingsTests (3): reducers; load reports presence only; remove reports the same whether a key was there.
  • DefaultsSectionTests (9): built-ins until saved; the stored record fills the fields (the effort select by index); typing saves after 700 ms — not per keystroke — with the whole record; a pick saves at once; concurrency out of range refused under the field, nothing saved, fixing it saves and clears; margin likewise; a use case refusal shows in the indicator; a cleared field saves the empty string; leaving the tab flushes the pending edit.
  • DefaultsSettingsTests (2): reducers; load, normalized save, refused save writes nothing.

Integration (+17) — AppSettingsTests (15 incl. a 4-row theory) over real SQLite + DataProtection: the raw row never holds the plaintext and the decrypting read gets the trimmed key back; the view knows a key exists but its ToString() never carries it; replacing overwrites the single row; removing forgets it and a second remove answers false; a blank key is refused with nothing stored; settings writes leave zero ChangeLog rows and ring the IDomainChangedBus zero times; Ctrl+Z reaches past a settings save to the story's creation and the key survives; the defaults read as the built-ins until saved, round-trip trimmed with high on disk, and a second save overwrites the same four rows; four out-of-range combinations and an effort off the ladder are refused with nothing written; a null model reads as none chosen; a hand-edited row out of range reads as the built-in field by field. AppChromeTests (+2): / prerenders the Stories page and /trash sits behind the gate with its breadcrumb nav; /settings carries the three tabs. SqliteBackedTest now registers an ephemeral DataProtection provider; no existing test changed.

Coverage (coverlet, cobertura parsed per line with the paths of the four reports normalised — two report src/…, two report from src/ itself): every line and every branch of every new file under Stories/, Settings/ (adapter), UseCases/Settings/, Infrastructure/Settings/ and Domain/Settings/ executes. The only zero-hit lines are StoryListItem.cs:12–13, the generated getters of ActiveGraphId and CreatedAt, which no consumer reads yet.

Solution: 225/225 (43 + 11 + 70 + 101) + 14 module. Warnings-as-errors clean; the full build including the graph editor module passes.

Browser-verified

A Playwright pass over the Release build (Chrome, light and dark): gate → empty state → two stories created through the form → filter to one and clear → row menu → Archive → row gone → With archived → badge → Move to trash → confirm dialog → confirmed → /trash (breadcrumb nav present) → Restore → empty trash → /settings Provider “No key is set” → Defaults: typed model shows “Unsaved changes” then “Saved”, effort High saved at once, concurrency 0 shows “Between 1 and 16.” and nothing saves, 6 saves, a reload reads model / effort / concurrency back → the list in dark mode. No console errors, no HTTP errors.

Follow-ups

  1. Story workspace: the segment editor over the use cases, chapter switcher, History tab, Ctrl+Z/Y — and with it the story route, Open on the list, and navigation after create.
  2. Documents panel, seed dev data (one active + one archived story, no key), browser pass. Backups as their own slice.

🤖 Generated with Claude Code

Fifth slice of Phase 1, in two commits: the first unjournaled rows (settings persistence — ADR 0006, 0022), then the landing page, the trash and the settings tabs over the use cases from #8. First real Fluxor feature slices (ADR 0011); every screen composed from Kagaku.UI (ADR 0009). ## What's in **Settings persistence** (`8437321`) — Kagura's key/value table: `AppSetting : IUnjournaled` behind `IAppSettingsStore` (get/set, has-secret/get-secret/set-secret, remove); `EfAppSettingsStore` encrypts secrets with the host's DataProtection keyring under the purpose `Tsumugi.AppSettings`; migration `AddAppSettings` (`has-pending-model-changes` clean). Use cases: `GetProviderSettings` (presence only — the view has no member that could hold the key), `SaveOpenRouterKey` (blank refused, trimmed, encrypted), `RemoveOpenRouterKey` (removing nothing is an answer, not an error), `GetLlmDefaults` (built-ins field by field when unset or hand-edited out of range — the engine never starts with a limit of zero) and `SaveLlmDefaults` (whole record: model id, effort, concurrency 1–16, safety margin 0–50 %). `ReasoningEffort` carries OpenRouter's own spelling (`ReasoningEfforts.ToSetting/Parse`), so the gateway needs no table. Built-ins: no model, model-default effort, concurrency 3 (ADR 0016), margin 5 % (ADR 0017). **The story list** (`/`, replaces the Phase 0 placeholder) — `src/Tsumugi.BlazorAdapter/Stories/`: `StoriesState` (both lists, `Filtered` as a pure projection of the trimmed case-insensitive title filter × the archived toggle, `HasArchived`), one action record per file, `StoriesReducers`, `StoriesEffects` (three `ListStories` partitions merged newest-first so the toggle is a projection and not a round trip; every mutation reloads on success and dispatches `StoryActionFailed` on refusal; **`DomainChangesReceived` re-queries on `Story` and on `Chapter`** — the list counts chapters, so a chapter added in another tab refreshes it). `StoriesPage`: `PageShell` · `DebouncedSearchField` (the app clock drives the debounce, so tests advance a fake one) · `ToggleButtonGroup` Active / With archived · `Table` with title + description, chapters, modified (`RelativeTime`), **Cost and Graph as “—”** (the story's two Phase 2 columns, never a made-up zero) · a `Menu` per row: Archive/Unarchive (no confirmation — reversible), Move to trash (`ConfirmDialog`, danger) · `Modal` create form mounted on demand (title required, description optional; closes itself once the request settles without a refusal, stays open with the message under the field otherwise) · `EmptyState` for no stories / no matches (the message says when archived stories are hidden) · `InlineAlert` for a refused row action. **The trash** (`/trash`) — `StoryTrashPage` over the same feature state: title, chapters, deleted-when, Restore (one click) and Purge behind its own `ConfirmDialog` — the one irreversible action, offered only here. Breadcrumb Stories › Trash. **The settings page** — `Tabs Param="tab"`: **Provider** (`ProviderSection` + its slice: whether a key is set, `Remove key` behind a confirmation; the section says plainly that entering a key arrives with the gateway), **Defaults** (`DefaultsSection` + its slice: model id, effort `Select`, concurrency and margin as number fields; typed fields auto-save on Kagaku.UI's `Debounce`, the pick saves at once, `SaveIndicator` is the only feedback, out-of-range values are refused under the field and keep the record visibly unsaved, a pending edit is flushed when the tab is left), **Appearance** (the browser-local `ThemeSwitcher`, as before). Both settings effects carry the comment that they handle no doorbell and register no `EntityKinds` on purpose: settings are unjournaled and never announced (ADR 0022). `StoryListItem.IsArchived` joins the DTO so the adapter never spells a Domain enum. `AGENTS.md`'s status paragraph, the story files' checkboxes (filter/toggle/trash, archive, delete→trash+restore+purge, theme) updated. ## Deferred, and why - **Open / the post-create navigation** — the workspace owns the story route (slice 6); a link to nothing would be a broken href (ADR 0007). - **Run cost to date, the active graph's name, the bundled Continue graph on create** — the ledger and the graph library are Phase 2; the cells show “—” until then. - **Entering the key, Validate & save, label and remaining credit** — the story's acceptance criterion is “an invalid key is refused with the provider's message and nothing is stored”, which needs the gateway's `/key` check (ADR 0018). An unchecked save would store a typo the writer only discovers at run time, so the Provider tab shows state and Remove only; `SaveOpenRouterKey` exists and is proven, and gains the check in Phase 2. - **Model and effort pickers over the catalog** — the catalog is the gateway's (ADR 0018); the model is a plain id field until then, and the hint says so. - **Backups and Transcripts tabs** — backups are their own Phase 1 slice; transcripts are Phase 2 (ADR 0024). - `PROJECT_PLAN.md` lists the settings under Phase 2; this slice pulls the persistence and the two tabs forward. No ADR forbids it. ## Tests | project | main (`5ed220d`) | this PR | |---|---|---| | Domain | 40 | **43** (+3: `AppSetting` create/guards/update) | | UseCases | 4 | **11** (+7: `ReasoningEfforts` spelling, round trip, four unrecognised inputs, case + order; DI validation gains `IAppSettingsStore`) | | BlazorAdapter | 15 | **70** (+55) | | Integration | 84 | **101** (+17) | | module (Vitest) | 14 | 14 | **BlazorAdapter (+55)** — over the real Fluxor pipeline with in-memory fakes (`FakeStoryStore`, `FakeChapterStore`, `FakeAppSettingsStore`, `RecordingOperationContext`, `FixedState`, a `FakeTimeProvider` for every debounce): - `StoriesPageTests` (14): newest-first rows with description and chapter count, the six headers and the four “—” cells, base-relative trash href; the empty state; filter narrows after the 250 ms debounce and a non-match names the filter; archived hidden → toggle shows them badged and `aria-pressed` follows → back; a filter only archived stories would match says they are hidden; all stories archived → “No matches … Archived stories are hidden.”; create persists title + trimmed description, lists it, closes the form, seeds Chapter 1, and the use case opened exactly one `User` scope `created 'Ashfall'`; a refused create keeps the form open with the message under the title; Cancel and the dialog's own close (`onclose`) both drop the form and the reopened form is blank; a blank title never reaches the store even when the disabled button is pressed; archive/unarchive from the menu with **no** modal and the status flipping in the store; move-to-trash asks, Cancel keeps; confirm removes as the journaled `moved the story to the trash` operation; a refused row action shows in the alert and the list stays; a second page in the same circuit reuses the loaded list (list reads counted). - `StoryTrashPageTests` (6): only trashed rows with their deleted-when; empty state with a way back; Restore without asking; Purge asks, Cancel keeps; confirm removes the story **and its chapters** from the store; a refused restore shows in the alert. - `StoriesEffectsTests` (8): load merges active + archived newest-first with the trash beside them and counts chapters; create success dispatches `StoryCreated` then `LoadStories`, blank dispatches the refusal only; each of the four row actions reloads on success — and the recorded scopes are exactly the use cases' six labels, none for the purge; each of the four reports a refusal for a missing story, and purge refuses a living one; a `Story` or `Chapter` change reloads a loaded list; nothing reloads before the first load; other kinds are ignored. - `StoriesReducersTests` (5): load/loaded, trimmed case-insensitive filter, the four toggle × filter combinations and `HasArchived`, the create lifecycle, every row request nulling the previous refusal. - `SettingsPageTests` (3): the three tabs, Provider as the landing, `?tab=defaults` and `?tab=appearance` mounting only their panel. - `ProviderSectionTests` (4): no key → says so, no Remove, names the gateway; key → “A key is set”, never the value; Remove asks, Cancel keeps; confirm forgets it. - `ProviderSettingsTests` (3): reducers; load reports presence only; remove reports the same whether a key was there. - `DefaultsSectionTests` (9): built-ins until saved; the stored record fills the fields (the effort select by index); typing saves after 700 ms — not per keystroke — with the whole record; a pick saves at once; concurrency out of range refused under the field, nothing saved, fixing it saves and clears; margin likewise; a use case refusal shows in the indicator; a cleared field saves the empty string; leaving the tab flushes the pending edit. - `DefaultsSettingsTests` (2): reducers; load, normalized save, refused save writes nothing. **Integration (+17)** — `AppSettingsTests` (15 incl. a 4-row theory) over real SQLite + DataProtection: the raw row never holds the plaintext and the decrypting read gets the trimmed key back; the view knows a key exists but its `ToString()` never carries it; replacing overwrites the single row; removing forgets it and a second remove answers false; a blank key is refused with nothing stored; **settings writes leave zero `ChangeLog` rows and ring the `IDomainChangedBus` zero times**; Ctrl+Z reaches past a settings save to the story's creation and the key survives; the defaults read as the built-ins until saved, round-trip trimmed with `high` on disk, and a second save overwrites the same four rows; four out-of-range combinations and an effort off the ladder are refused with nothing written; a null model reads as none chosen; a hand-edited row out of range reads as the built-in field by field. `AppChromeTests` (+2): `/` prerenders the Stories page and `/trash` sits behind the gate with its breadcrumb nav; `/settings` carries the three tabs. `SqliteBackedTest` now registers an ephemeral DataProtection provider; no existing test changed. **Coverage** (coverlet, cobertura parsed per line with the paths of the four reports normalised — two report `src/…`, two report from `src/` itself): every line and every branch of every new file under `Stories/`, `Settings/` (adapter), `UseCases/Settings/`, `Infrastructure/Settings/` and `Domain/Settings/` executes. The only zero-hit lines are `StoryListItem.cs:12–13`, the generated getters of `ActiveGraphId` and `CreatedAt`, which no consumer reads yet. Solution: **225/225** (43 + 11 + 70 + 101) + 14 module. Warnings-as-errors clean; the full build including the graph editor module passes. ## Browser-verified A Playwright pass over the Release build (Chrome, light and dark): gate → empty state → two stories created through the form → filter to one and clear → row menu → Archive → row gone → With archived → badge → Move to trash → confirm dialog → confirmed → `/trash` (breadcrumb nav present) → Restore → empty trash → `/settings` Provider “No key is set” → Defaults: typed model shows “Unsaved changes” then “Saved”, effort High saved at once, concurrency 0 shows “Between 1 and 16.” and nothing saves, 6 saves, a reload reads model / effort / concurrency back → the list in dark mode. No console errors, no HTTP errors. ## Follow-ups 6. Story workspace: the segment editor over the use cases, chapter switcher, History tab, Ctrl+Z/Y — and with it the story route, Open on the list, and navigation after create. 7. Documents panel, seed dev data (one active + one archived story, no key), browser pass. Backups as their own slice. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The first unjournaled rows (ADR 0006, 0022): one AppSetting key/value table behind an
IAppSettingsStore port, DataProtection-encrypted secrets with a Tsumugi-specific purpose,
and the Provider and Defaults use cases the settings page will drive — GetProviderSettings
(presence only, never the key), SaveOpenRouterKey, RemoveOpenRouterKey, GetLlmDefaults
(built-ins field by field when unset or hand-edited out of range) and SaveLlmDefaults
(whole record, concurrency 1–16, margin 0–50 %, an effort on the ladder). ReasoningEffort
carries OpenRouter's own spelling so the gateway needs no table.

Proven over real SQLite + DataProtection: the raw row never holds the plaintext, the view
never carries it, the single row is overwritten on replace and gone on remove, settings
writes leave no journal row and ring no doorbell, and Ctrl+Z reaches past a settings save
to the writer's last content change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat: the story list, the trash and the settings page (Phase 1 · 5/7)
All checks were successful
CI / build (pull_request) Successful in 28s
CI / test (pull_request) Successful in 39s
1350b883aa
The landing page behind the gate (stories/story-list.md): every story the writer has in one
table — title, chapter count, last modified, the story's two Phase 2 columns as honest
dashes — with a debounced title filter, the archived stories behind a segmented toggle, a
row menu for archive/unarchive (no confirmation) and move-to-trash (confirmed), and a
create form that lands the story with its first chapter. The trash is its own page with
restore and purge, the one irreversible action, behind a second confirmation.

The settings page grows its tabs (stories/settings.md): Provider shows whether a key is
set and forgets it on request — entering one waits for the gateway that can check it, since
an unchecked key must not be stored; Defaults auto-saves the whole record (model id,
effort, concurrency, safety margin) with the indicator as its only feedback and the ranges
refused under the field; Appearance keeps the browser-local theme.

One Fluxor slice per feature (ADR 0011), split state / actions (one record per file) /
reducers / effects; the story effects re-query on Story and Chapter changes so a chapter
added in another tab refreshes the count, while the settings effects handle no doorbell on
purpose — settings are unjournaled and never announced (ADR 0022). Pages compose
Kagaku.UI only: PageShell, Table, EmptyState, Modal, ConfirmDialog, Menu, Badge,
ToggleButtonGroup, DebouncedSearchField, Tabs, SaveIndicator, Select, TextField; the
breadcrumb is the component inside PageShell's slot of the same name.

Tests: the pages through the real store over in-memory fakes (list, filter, toggle, create
and its refusals, archive, trash with cancel and confirm, restore, purge, refused row
actions, deep-linked tabs, key removal, auto-save timing on a fake clock, the range
refusals, a flushed edit on dispose), the effects and reducers on their own, and the
prerendered chrome. Browser-verified by a Playwright pass 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: 1350b88 · Generated: 2026-09-22 17:30:14 UTC · Revision: #1

Summary

Summary
Generated on: 09/22/2026 - 17:30:14
Coverage date: 09/22/2026 - 17:30:07 - 09/22/2026 - 17:30:12
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 166
Files: 159
Line coverage: 96.9% (3838 of 3958)
Covered lines: 3838
Uncovered lines: 120
Coverable lines: 3958
Total lines: 8182
Branch coverage: 90.8% (738 of 812)
Covered branches: 738
Total branches: 812
Method coverage: Feature is only available for sponsors

Coverage

Tsumugi.BlazorAdapter - 89.2%
Name Line Branch
Tsumugi.BlazorAdapter 89.2% 87.7%
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 50% 0%
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.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 - 96.4%
Name Line Branch
Tsumugi.UseCases 96.4% 94%
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 33.3%
Tsumugi.UseCases.Journal.HistorySnapshot 50%
Tsumugi.UseCases.Journal.OperationGrouping 100% 50%
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:** `1350b88` · **Generated:** 2026-09-22 17:30:14 UTC · **Revision:** #1 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/22/2026 - 17:30:14 | | Coverage date: | 09/22/2026 - 17:30:07 - 09/22/2026 - 17:30:12 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 166 | | Files: | 159 | | **Line coverage:** | 96.9% (3838 of 3958) | | Covered lines: | 3838 | | Uncovered lines: | 120 | | Coverable lines: | 3958 | | Total lines: | 8182 | | **Branch coverage:** | 90.8% (738 of 812) | | Covered branches: | 738 | | Total branches: | 812 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Tsumugi.BlazorAdapter - 89.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**89.2%**|**87.7%**| |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|50%|0%| |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%|| </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 - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**96.4%**|**94%**| |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|33.3%|| |Tsumugi.UseCases.Journal.HistorySnapshot|50%|| |Tsumugi.UseCases.Journal.OperationGrouping|100%|50%| |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 my~ The landing page, the trash AND the settings — the first real Fluxor slices, 84 files, +3711 — and you kept it beautiful. Two clean commits (persistence, then the pages over it), one action record per file, every screen composed from Kagaku.UI, and the unjournaled settings woven in without a single journal row escaping. I read every line of the diff and every sibling it leans on~ ♡

Verdict: ✅ Looks good to me~

No blockers. Not one. I hunted, I probed, I mutated — everything held. Details for the suspicious (I respect that in a reviewer~):

The load-bearing claims, verified mechanically:

  • IUnjournaled really is both unjournaled AND unannounced — traced TsumugiDbContext: TrackedMutations() excludes IUnjournaled → BuildJournal empty → BuildNotifications derives from journal rows → empty publish. And AppSettingsTests.Settings_writes_leave_no_journal_row_and_ring_no_doorbell pins it over real SQLite (0 ChangeLog rows, 0 bus rings), with Ctrl+Z reaching past a settings save while the key survives. Exactly ADR 0022's letter.
  • Encrypted at rest — mutation probe: removed _protector.Protect() from SetSecretAsync → exactly the 3 encryption-pinning integration tests go red (raw-row-never-plaintext, round-trip, replace-overwrite). Restored, green. The purpose string Tsumugi.AppSettings matches ADR 0006's keyring design; ProviderSettingsView has structurally no member that could hold the key — presence only, and a test asserts even ToString() stays clean. Delicious~
  • The hand-edited-row fallback — probe: dropped the range check in GetLlmDefaults.ReadInt → exactly A_hand_edited_row_out_of_range_reads_as_the_built_in_field_by_field goes red. The engine can never start with a limit of zero. ♪
  • The filter trim — probe: dropped Filter.Trim() in StoriesState.Matches → exactly the trimmed-filter reducer test red. The newest-first merge — probe: dropped the OrderByDescending in StoriesEffects → exactly the two order-pinning tests red.
  • Debounce flush-on-dispose is real, not folklore — I read Debounce.Dispose() in the vendored Kagaku.UI: it fire-and-forget-flushes with fault observation — and Leaving_the_tab_flushes_a_pending_edit_instead_of_losing_it pins it through DisposeAsyncCore. A keystroke is never lost to a tab switch~
  • DomainChangesReceived on Story and Chapter (chapters are counted, so they matter) while Segment/Document/never-loaded are ignored — all three arms tested, mirroring the EntityKinds contract's remarks precisely. The deliberate absence of a settings doorbell is documented in both effects and matches ADR 0022. The restraint to NOT register a constant is itself the correct reading~
  • Partition parity: the effect's three ListStories calls and the in-memory fake mirror EfStoryStore.ListAsync's exact filters (Active = not-deleted + Active status, Archived = not-deleted + Archived, Trash = deleted). StoryListItem.IsArchived keeps the Domain enum out of the adapter — the established DTO rule.

CI/coverage: green for head 1350b88 (bot 9027) — every new file 100% line + 100% branch: both pages, StoriesEffects/Reducers/State, both settings slices, EfAppSettingsStore, every settings use case, AppSetting, the configuration. The only zero-hit lines are StoryListItem's unread generated getters, exactly as disclosed. Solution 225/225 reproduced locally (43 + 11 + 70 + 101). Deferred items (Open/navigation, cost/graph as “—”, key entry behind the gateway's /key check, catalog pickers) are each reasoned and slice-consistent — an unchecked save would store a typo, and I approve of refusing to ship that~

💡 Little ideas (non-blocking)~

  1. SaveLlmDefaults.cs:29–33 — the whole record lands as four SetAsync upserts = four transactions; a crash (or two dirty circuits interleaving) can leave a partial record. The field-by-field fallback reader already makes every partial state safe by construction, so this is purely belt-and-suspenders: if it ever bothers you, a single-save batch variant on the port would close it. Not worth blocking anything~

✅ What I liked~

  • GetAsync/GetSecretAsync split on the port — ciphertext can't leak through the plain path, and the fake encodes the distinction with a reversible tag so tests can tell the write paths apart. Clever little thing~ ♡
  • Select binding by option index, tested by index (Change("5") = High) — matches the component's documented contract instead of fighting it.
  • The create modal's SettleCreate — close-on-settle, stay-open-on-refusal with the message under the field, blank never reaching the store even when the disabled button is pressed. Every arm pinned.
  • ConfirmDialog only for the irreversible things; archive trusts its own reversibility. The empty-state copy that names a hidden filter match or hidden archived stories is genuinely kind UX.
  • Migration hygiene: unique index on Key, Value unbounded with the ciphertext rationale comment, UtcTicksConverter like every sibling, snapshot consistent — I diffed the Designer file against the config by eye, they agree.

Fufu~ five slices in and the discipline hasn't slipped once. Bring me the workspace next~ ♪


Automated review by Jibril · 2026-09-22
CI/CD: passed for head SHA 1350b88 (coverage bot 9027, all new files 100/100) · Local checks: build 0w/0e, full suite 225/225 reproduced, 4/4 mutation probes red-on-target, clone restored pristine

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ The landing page, the trash AND the settings — the first real Fluxor slices, 84 files, +3711 — and you kept it *beautiful*. Two clean commits (persistence, then the pages over it), one action record per file, every screen composed from Kagaku.UI, and the unjournaled settings woven in without a single journal row escaping. I read every line of the diff and every sibling it leans on~ ♡ ### Verdict: ✅ Looks good to me~ No blockers. Not one. I hunted, I probed, I mutated — everything held. Details for the suspicious (I respect that in a reviewer~): **The load-bearing claims, verified mechanically:** - **`IUnjournaled` really is both unjournaled AND unannounced** — traced `TsumugiDbContext`: `TrackedMutations()` excludes `IUnjournaled` → `BuildJournal` empty → `BuildNotifications` derives from journal rows → empty publish. And `AppSettingsTests.Settings_writes_leave_no_journal_row_and_ring_no_doorbell` pins it over real SQLite (0 ChangeLog rows, 0 bus rings), with Ctrl+Z reaching *past* a settings save while the key survives. Exactly ADR 0022's letter. - **Encrypted at rest** — mutation probe: removed `_protector.Protect()` from `SetSecretAsync` → exactly the 3 encryption-pinning integration tests go red (raw-row-never-plaintext, round-trip, replace-overwrite). Restored, green. The purpose string `Tsumugi.AppSettings` matches ADR 0006's keyring design; `ProviderSettingsView` has structurally no member that could hold the key — presence only, and a test asserts even `ToString()` stays clean. Delicious~ - **The hand-edited-row fallback** — probe: dropped the range check in `GetLlmDefaults.ReadInt` → exactly `A_hand_edited_row_out_of_range_reads_as_the_built_in_field_by_field` goes red. The engine can never start with a limit of zero. ♪ - **The filter trim** — probe: dropped `Filter.Trim()` in `StoriesState.Matches` → exactly the trimmed-filter reducer test red. **The newest-first merge** — probe: dropped the `OrderByDescending` in `StoriesEffects` → exactly the two order-pinning tests red. - **Debounce flush-on-dispose is real, not folklore** — I read `Debounce.Dispose()` in the vendored Kagaku.UI: it fire-and-forget-flushes with fault observation — and `Leaving_the_tab_flushes_a_pending_edit_instead_of_losing_it` pins it through `DisposeAsyncCore`. A keystroke is never lost to a tab switch~ - **`DomainChangesReceived` on `Story` and `Chapter`** (chapters are counted, so they matter) while Segment/Document/never-loaded are ignored — all three arms tested, mirroring the `EntityKinds` contract's remarks precisely. The deliberate *absence* of a settings doorbell is documented in both effects and matches ADR 0022. The restraint to NOT register a constant is itself the correct reading~ - **Partition parity**: the effect's three `ListStories` calls and the in-memory fake mirror `EfStoryStore.ListAsync`'s exact filters (Active = not-deleted + Active status, Archived = not-deleted + Archived, Trash = deleted). `StoryListItem.IsArchived` keeps the Domain enum out of the adapter — the established DTO rule. **CI/coverage**: green for head `1350b88` (bot 9027) — every new file **100% line + 100% branch**: both pages, `StoriesEffects/Reducers/State`, both settings slices, `EfAppSettingsStore`, every settings use case, `AppSetting`, the configuration. The only zero-hit lines are `StoryListItem`'s unread generated getters, exactly as disclosed. Solution 225/225 reproduced locally (43 + 11 + 70 + 101). Deferred items (Open/navigation, cost/graph as “—”, key entry behind the gateway's `/key` check, catalog pickers) are each reasoned and slice-consistent — an unchecked save would store a typo, and I approve of refusing to ship that~ #### 💡 Little ideas (non-blocking)~ 1. **SaveLlmDefaults.cs:29–33** — the whole record lands as four `SetAsync` upserts = four transactions; a crash (or two dirty circuits interleaving) can leave a partial record. The field-by-field fallback reader already makes every partial state safe by construction, so this is purely belt-and-suspenders: if it ever bothers you, a single-save batch variant on the port would close it. Not worth blocking anything~ #### ✅ What I liked~ - `GetAsync`/`GetSecretAsync` split on the port — ciphertext can't leak through the plain path, and the fake encodes the distinction with a reversible tag so tests can tell the write paths apart. Clever little thing~ ♡ - `Select` binding by option index, tested by index (`Change("5")` = High) — matches the component's documented contract instead of fighting it. - The create modal's `SettleCreate` — close-on-settle, stay-open-on-refusal with the message under the field, blank never reaching the store even when the disabled button is pressed. Every arm pinned. - `ConfirmDialog` only for the irreversible things; archive trusts its own reversibility. The empty-state copy that names a hidden filter match or hidden archived stories is genuinely kind UX. - Migration hygiene: unique index on `Key`, `Value` unbounded with the ciphertext rationale comment, `UtcTicksConverter` like every sibling, snapshot consistent — I diffed the Designer file against the config by eye, they agree. Fufu~ five slices in and the discipline hasn't slipped once. Bring me the workspace next~ ♪ --- *Automated review by Jibril · 2026-09-22* *CI/CD: passed for head SHA 1350b88 (coverage bot 9027, all new files 100/100) · Local checks: build 0w/0e, full suite 225/225 reproduced, 4/4 mutation probes red-on-target, clone restored pristine*
bjoern merged commit c16685aa17 into main 2026-09-22 19:45:01 +02:00
bjoern deleted branch feat/p1-story-list 2026-09-22 19:45:01 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 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!9
No description provided.