Characters: the first record type — list, create-empty-and-open, editor stub #19

Merged
bjoern merged 2 commits from feat/characters-list into main 2026-07-10 07:47:04 +02:00
Member

The characters-list story — and with it, the graph foundation's first real payload: Character is an Entry-derived node via TPT (the Characters table shares the Entry PK), so links and backlinks work for it like for any node. The raw DB shows ADR 0019's shared-PK design working: the same GUID lives in both Entries (Kind=Character) and Characters.

Domain

  • Entry titles may now be empty (trimmed; null still rejected) — the story's create-empty-and-open means records start untitled, and UIs render an italic "(unnamed)". Documented on Entry.Edit; the graph tests updated accordingly.
  • Character : Entry in the KnowledgeBase module (ADR 0017) with no specialized columns yet — the editor story adds them (role, personality, visual data, …), each with its own migration. AddCharacters migration: Characters.Id FK → Entries.Id.

Use cases

  • CreateCharacter — a blank record, immediately persisted and journaled; no form precedes it (unlike project creation). An abandoned empty character is a normal record.
  • ListCharacters (project-scoped, modified-first) · GetCharacter · ICharacterStore/EfCharacterStore.

UI

  • CharactersPage replaces the section stub: debounced name filter, table with a person-placeholder preview + name (unnamed fallback), Create → the effect persists a blank record and navigates straight to its editor. Row click opens the character.
  • The Characters Fluxor feature handles the multi-project reality: switching projects resets the list + filter, and a stale load for a departed project is ignored.
  • CharacterEditorPage is a deliberate stub — it anchors the route (/projects/{slug}/characters/{id:guid}) and the four-level breadcrumb contract (Projects › name › Characters › leaf; WorkspaceShell gains SectionHref + Leaf params) plus a minimal editor state the character-editor story builds on. Not-found state included.

Tests — +18 (264 total)

  • Integration (real SQLite): the TPT round-trip — a blank character persists, lists, journals as a Character Create, and Undo removes it; project scoping (two projects, separate casts); and the milestone: a character links to a location entry and resolves in backlinks through the real store (residents ← the character, Kind=Character).
  • bUnit (real Fluxor pipeline): list with unnamed fallback, empty state, filter, create → persisted blank + navigated to its editor, row-click open, editor breadcrumb leaf, unknown-character not-found.
  • Unit: reducers (project-switch reset, stale-load guard, filter, prepend) and effects over real use cases.

Browser walk (Chrome)

Login → create project → Characters (empty state) → Create character → landed on the blank editor with the four-level breadcrumb and "(unnamed)" header → back via breadcrumb → the list shows the unnamed row. DB check: same GUID in Entries + Characters, journal = Project Create + Character Create. Both themes screenshotted.

Verification

Build Debug + Release — 0 warnings / 0 errors · 264/264 tests · browser walk incl. raw-DB TPT check.

Not in this PR

The character editor itself (tabs, quicklinks, labeled-entries tables, connections panel — the character-editor story) · character delete (belongs to the editor's header per that story) · preview images (asset-storage foundation).

🤖 Generated with Claude Code

The [characters-list story](https://git.kagaku.eu/TeamAI/Kagura/src/branch/main/docs/stories/characters-list.md) — and with it, **the graph foundation's first real payload**: `Character` is an `Entry`-derived node via **TPT** (the `Characters` table shares the Entry PK), so links and backlinks work for it like for any node. The raw DB shows ADR 0019's shared-PK design working: the same GUID lives in both `Entries` (Kind=Character) and `Characters`. ## Domain - **Entry titles may now be empty** (trimmed; `null` still rejected) — the story's create-empty-and-open means records start untitled, and UIs render an italic **"(unnamed)"**. Documented on `Entry.Edit`; the graph tests updated accordingly. - `Character : Entry` in the **KnowledgeBase module** (ADR 0017) with *no specialized columns yet* — the editor story adds them (role, personality, visual data, …), each with its own migration. `AddCharacters` migration: `Characters.Id` FK → `Entries.Id`. ## Use cases - `CreateCharacter` — a **blank record, immediately persisted and journaled**; no form precedes it (unlike project creation). An abandoned empty character is a normal record. - `ListCharacters` (project-scoped, modified-first) · `GetCharacter` · `ICharacterStore`/`EfCharacterStore`. ## UI - **`CharactersPage`** replaces the section stub: debounced name filter, table with a person-placeholder preview + name (unnamed fallback), **Create → the effect persists a blank record and navigates straight to its editor**. Row click opens the character. - The Characters Fluxor feature handles the multi-project reality: **switching projects resets the list + filter**, and a **stale load for a departed project is ignored**. - **`CharacterEditorPage`** is a deliberate stub — it anchors the route (`/projects/{slug}/characters/{id:guid}`) and the four-level breadcrumb contract (**Projects › name › Characters › leaf**; `WorkspaceShell` gains `SectionHref` + `Leaf` params) plus a minimal editor state the character-editor story builds on. Not-found state included. ## Tests — +18 (264 total) - **Integration (real SQLite):** the TPT round-trip — a blank character persists, lists, journals as a `Character` **Create**, and **`Undo` removes it**; project scoping (two projects, separate casts); and the milestone: **a character links to a location entry and resolves in backlinks through the real store** (`residents` ← the character, Kind=Character). - **bUnit (real Fluxor pipeline):** list with unnamed fallback, empty state, filter, **create → persisted blank + navigated to its editor**, row-click open, editor breadcrumb leaf, unknown-character not-found. - **Unit:** reducers (project-switch reset, stale-load guard, filter, prepend) and effects over real use cases. ## Browser walk (Chrome) Login → create project → Characters (empty state) → **Create character → landed on the blank editor** with the four-level breadcrumb and "(unnamed)" header → back via breadcrumb → the list shows the unnamed row. **DB check:** same GUID in `Entries` + `Characters`, journal = Project Create + Character Create. Both themes screenshotted. ## Verification Build Debug + Release — 0 warnings / 0 errors · 264/264 tests · browser walk incl. raw-DB TPT check. ## Not in this PR The character editor itself (tabs, quicklinks, labeled-entries tables, connections panel — the character-editor story) · character delete (belongs to the editor's header per that story) · preview images (asset-storage foundation). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(characters): the first record type — list, create-empty-and-open, editor stub
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 22s
e910dd105a
The characters-list story, and with it the graph foundation's first real payload:
Character is an Entry-derived node (TPT — Characters shares the Entry PK), so links
and backlinks work for it like for any node.

- Domain: Entry titles may now be empty (trimmed; null still rejected) — records are
  created blank and UIs render "(unnamed)"; Character : Entry in the KnowledgeBase
  module with no specialized columns yet (the editor story adds them); TPT mapping
  (UseTptMappingStrategy) + AddCharacters migration (Characters.Id FK -> Entries.Id).
- UseCases (KnowledgeBase): ICharacterStore; CreateCharacter (blank, immediately
  persisted + journaled — no form precedes it), ListCharacters (project-scoped,
  modified-first), GetCharacter; EfCharacterStore.
- UI: Characters Fluxor feature (project-switch resets the list + filter; a stale
  load for a departed project is ignored); CharactersPage replaces the section stub
  (debounced name filter, table with person-placeholder preview + name with an
  italic "(unnamed)" fallback, Create -> effect persists a blank record and
  navigates straight to its editor); CharacterEditorPage stub anchors the route +
  breadcrumb contract (Projects > name > Characters > leaf — WorkspaceShell gains
  SectionHref + Leaf params) with its own minimal editor state the editor story
  builds on.
- Tests: +18 (264 total). Integration proves the TPT round-trip (create journaled as
  a Character Create, Undo removes it), project scoping, and — the milestone — a
  character linking to a location entry with backlinks resolving through the real
  store. bUnit covers list/filter/empty state/unnamed fallback/create-navigate/row
  open/editor breadcrumb/not-found.
- Verified in the real browser: Create from the empty state landed on the blank
  editor with the four-level breadcrumb; the raw DB shows the same GUID in Entries
  and Characters (the shared-PK node identity) and the journaled create. Both themes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 07/10/2026 - 05:46:23
Coverage date: 07/10/2026 - 05:46:17 - 07/10/2026 - 05:46:21
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 134
Files: 115
Line coverage: 93.2% (2840 of 3046)
Covered lines: 2840
Uncovered lines: 206
Coverable lines: 3046
Total lines: 6267
Branch coverage: 86.1% (484 of 562)
Covered branches: 484
Total branches: 562
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 67.9%
Name Line Branch
Kagura.BlazorAdapter 67.9% 74.3%
Kagura.BlazorAdapter.BlazorAdapterAssembly 100%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage 86.6% 72.7%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects 90.9% 50%
Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersPage 90% 75%
Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers 100% 87.5%
Kagura.BlazorAdapter.KnowledgeBase.CharactersState 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed 0%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters 100%
Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter 100%
Kagura.BlazorAdapter.OverlayDemo 0% 0%
Kagura.BlazorAdapter.Projects.CreateProjectRequested 100%
Kagura.BlazorAdapter.Projects.DeleteProjectRequested 100%
Kagura.BlazorAdapter.Projects.LoadWorkspace 100%
Kagura.BlazorAdapter.Projects.ProjectCreated 100%
Kagura.BlazorAdapter.Projects.ProjectCreateFailed 100%
Kagura.BlazorAdapter.Projects.ProjectDeleted 100%
Kagura.BlazorAdapter.Projects.ProjectSaved 100%
Kagura.BlazorAdapter.Projects.ProjectSaveFailed 100%
Kagura.BlazorAdapter.Projects.ProjectsEffects 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsLoaded 100%
Kagura.BlazorAdapter.Projects.ProjectsPage 94.7% 100%
Kagura.BlazorAdapter.Projects.ProjectsReducers 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsState 100% 100%
Kagura.BlazorAdapter.Projects.ProjectWorkspacePage 93.7% 75%
Kagura.BlazorAdapter.Projects.SaveProjectRequested 100%
Kagura.BlazorAdapter.Projects.SetProjectsFilter 100%
Kagura.BlazorAdapter.Projects.WorkspaceEffects 100% 100%
Kagura.BlazorAdapter.Projects.WorkspaceLoaded 100%
Kagura.BlazorAdapter.Projects.WorkspaceReducers 100%
Kagura.BlazorAdapter.Projects.WorkspaceSectionPage 95.2% 66.6%
Kagura.BlazorAdapter.Projects.WorkspaceShell 100% 93.7%
Kagura.BlazorAdapter.Projects.WorkspaceState 100%
Kagura.Domain - 96.4%
Name Line Branch
Kagura.Domain 96.4% 83.9%
Kagura.Domain.Graph.Entry 100% 100%
Kagura.Domain.Graph.Link 100% 100%
Kagura.Domain.Graph.LinkRole 100% 100%
Kagura.Domain.Graph.LinkRoles 92.3%
Kagura.Domain.Journal.ChangeLogEntry 100%
Kagura.Domain.KnowledgeBase.Character 100%
Kagura.Domain.Projects.Project 100% 100%
Kagura.Domain.Projects.Slug 100% 100%
System.Text.RegularExpressions.Generated 90.2% 72.2%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
89.4% 75%
Kagura.Infrastructure - 96.1%
Name Line Branch
Kagura.Infrastructure 96.1% 88%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 95.5% 66.6%
Kagura.Infrastructure.Journal.EfChangeJournal 100%
Kagura.Infrastructure.Journal.EfUndoStore 97.5% 90.6%
Kagura.Infrastructure.Journal.OperationContext 100% 100%
Kagura.Infrastructure.KnowledgeBase.EfCharacterStore 100%
Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio
n
100%
Kagura.Infrastructure.Persistence.Configurations.CharacterConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Kagura.Infrastructure.Persistence.KaguraDbContext 85.2% 85.2%
Kagura.Infrastructure.Persistence.KaguraDbContextFactory 0%
Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag 96.8%
Kagura.Infrastructure.Persistence.Migrations.AddCharacters 98.7%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription 98.1%
Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog 90.3%
Kagura.Infrastructure.Persistence.Migrations.InitialCreate 94.4%
Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot 100%
Kagura.Infrastructure.Projects.EfProjectStore 100% 100%
Kagura.Kernel - 90%
Name Line Branch
Kagura.Kernel 90% 75%
Kagura.Kernel.Err`1 100%
Kagura.Kernel.Ok`1 100%
Kagura.Kernel.Result`1 87.5% 75%
Kagura.Server - 100%
Name Line Branch
Kagura.Server 100% 78.9%
Kagura.Server.Components.App 100%
Kagura.Server.Components.Layout.MainLayout 100%
Kagura.Server.Components.Pages.Error 100% 50%
Kagura.Server.Components.Pages.Gate 100% 100%
Kagura.Server.Security.AccessGate 100% 83.3%
Kagura.Server.Security.AccessSecret 100% 100%
Program 100% 80%
Kagura.UI - 97.7%
Name Line Branch
Kagura.UI 97.7% 94.4%
Kagura.UI.Badge 100% 100%
Kagura.UI.Breadcrumb 100%
Kagura.UI.BreadcrumbItem 100% 100%
Kagura.UI.Button 100% 100%
Kagura.UI.Card 100% 100%
Kagura.UI.ConfirmDialog 100%
Kagura.UI.CssClassExtensions 100%
Kagura.UI.DebouncedSearchField 100% 88.8%
Kagura.UI.EmptyState 100% 100%
Kagura.UI.Field 100% 100%
Kagura.UI.Icon 100% 100%
Kagura.UI.IconCatalog 100%
Kagura.UI.InputFieldBase 94.2% 87.5%
Kagura.UI.Modal 87.1% 90%
Kagura.UI.NavGroup 100% 100%
Kagura.UI.NavItem 100% 100%
Kagura.UI.NavList 100%
Kagura.UI.PreviewImage 100% 100%
Kagura.UI.RelativeTime 100% 93.7%
Kagura.UI.Separator 100%
Kagura.UI.StatusDot 100%
Kagura.UI.Table`1 100% 92.3%
Kagura.UI.TableColumn`1 100%
Kagura.UI.TextArea 100% 100%
Kagura.UI.TextField 100%
Kagura.UseCases - 95.6%
Name Line Branch
Kagura.UseCases 95.6% 96.1%
Kagura.UseCases.DependencyInjection 100%
Kagura.UseCases.Graph.EdgeGroup 100%
Kagura.UseCases.Graph.GetNodeGraph 96.4% 83.3%
Kagura.UseCases.Graph.GraphEdgeView 85.7%
Kagura.UseCases.Graph.LinkNodes 100% 100%
Kagura.UseCases.Graph.NodeGraphView 100%
Kagura.UseCases.Graph.NodeSummary 100%
Kagura.UseCases.Graph.RemoveLink 100% 100%
Kagura.UseCases.Graph.RestoreLink 100% 100%
Kagura.UseCases.Journal.ChangeRecordView 42.8%
Kagura.UseCases.Journal.GetEntityHistory 100%
Kagura.UseCases.Journal.GetUndoStatus 100%
Kagura.UseCases.Journal.Redo 100% 100%
Kagura.UseCases.Journal.Undo 100% 100%
Kagura.UseCases.Journal.UndoOutcome 100%
Kagura.UseCases.Journal.UndoStatus 100%
Kagura.UseCases.KnowledgeBase.CharacterDto 80%
Kagura.UseCases.KnowledgeBase.CreateCharacter 100%
Kagura.UseCases.KnowledgeBase.GetCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.ListCharacters 100%
Kagura.UseCases.Projects.CreateProject 100% 100%
Kagura.UseCases.Projects.DeleteProject 100% 100%
Kagura.UseCases.Projects.GetProject 100% 100%
Kagura.UseCases.Projects.ListProjects 100%
Kagura.UseCases.Projects.ProjectDto 100%
Kagura.UseCases.Projects.UpdateProject 100% 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/10/2026 - 05:46:23 | | Coverage date: | 07/10/2026 - 05:46:17 - 07/10/2026 - 05:46:21 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 134 | | Files: | 115 | | **Line coverage:** | 93.2% (2840 of 3046) | | Covered lines: | 2840 | | Uncovered lines: | 206 | | Coverable lines: | 3046 | | Total lines: | 6267 | | **Branch coverage:** | 86.1% (484 of 562) | | Covered branches: | 484 | | Total branches: | 562 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 67.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**67.9%**|**74.3%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|100%|| |Kagura.BlazorAdapter.Design|0%|0%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage|86.6%|72.7%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects|90.9%|50%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersPage|90%|75%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers|100%|87.5%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersState|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed|0%|| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter|100%|| |Kagura.BlazorAdapter.OverlayDemo|0%|0%| |Kagura.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.DeleteProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.LoadWorkspace|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreated|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreateFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectDeleted|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaved|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaveFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectsEffects|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsLoaded|100%|| |Kagura.BlazorAdapter.Projects.ProjectsPage|94.7%|100%| |Kagura.BlazorAdapter.Projects.ProjectsReducers|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsState|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectWorkspacePage|93.7%|75%| |Kagura.BlazorAdapter.Projects.SaveProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.SetProjectsFilter|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceEffects|100%|100%| |Kagura.BlazorAdapter.Projects.WorkspaceLoaded|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceReducers|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceSectionPage|95.2%|66.6%| |Kagura.BlazorAdapter.Projects.WorkspaceShell|100%|93.7%| |Kagura.BlazorAdapter.Projects.WorkspaceState|100%|| </details> <details><summary>Kagura.Domain - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**96.4%**|**83.9%**| |Kagura.Domain.Graph.Entry|100%|100%| |Kagura.Domain.Graph.Link|100%|100%| |Kagura.Domain.Graph.LinkRole|100%|100%| |Kagura.Domain.Graph.LinkRoles|92.3%|| |Kagura.Domain.Journal.ChangeLogEntry|100%|| |Kagura.Domain.KnowledgeBase.Character|100%|| |Kagura.Domain.Projects.Project|100%|100%| |Kagura.Domain.Projects.Slug|100%|100%| |System.Text.RegularExpressions.Generated|90.2%|72.2%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484<br/>D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0|89.4%|75%| </details> <details><summary>Kagura.Infrastructure - 96.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**96.1%**|**88%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|95.5%|66.6%| |Kagura.Infrastructure.Journal.EfChangeJournal|100%|| |Kagura.Infrastructure.Journal.EfUndoStore|97.5%|90.6%| |Kagura.Infrastructure.Journal.OperationContext|100%|100%| |Kagura.Infrastructure.KnowledgeBase.EfCharacterStore|100%|| |Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio<br/>n|100%|| |Kagura.Infrastructure.Persistence.Configurations.CharacterConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Kagura.Infrastructure.Persistence.KaguraDbContext|85.2%|85.2%| |Kagura.Infrastructure.Persistence.KaguraDbContextFactory|0%|| |Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag|96.8%|| |Kagura.Infrastructure.Persistence.Migrations.AddCharacters|98.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription|98.1%|| |Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog|90.3%|| |Kagura.Infrastructure.Persistence.Migrations.InitialCreate|94.4%|| |Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot|100%|| |Kagura.Infrastructure.Projects.EfProjectStore|100%|100%| </details> <details><summary>Kagura.Kernel - 90%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Kernel**|**90%**|**75%**| |Kagura.Kernel.Err`1|100%|| |Kagura.Kernel.Ok`1|100%|| |Kagura.Kernel.Result`1|87.5%|75%| </details> <details><summary>Kagura.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Server**|**100%**|**78.9%**| |Kagura.Server.Components.App|100%|| |Kagura.Server.Components.Layout.MainLayout|100%|| |Kagura.Server.Components.Pages.Error|100%|50%| |Kagura.Server.Components.Pages.Gate|100%|100%| |Kagura.Server.Security.AccessGate|100%|83.3%| |Kagura.Server.Security.AccessSecret|100%|100%| |Program|100%|80%| </details> <details><summary>Kagura.UI - 97.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**97.7%**|**94.4%**| |Kagura.UI.Badge|100%|100%| |Kagura.UI.Breadcrumb|100%|| |Kagura.UI.BreadcrumbItem|100%|100%| |Kagura.UI.Button|100%|100%| |Kagura.UI.Card|100%|100%| |Kagura.UI.ConfirmDialog|100%|| |Kagura.UI.CssClassExtensions|100%|| |Kagura.UI.DebouncedSearchField|100%|88.8%| |Kagura.UI.EmptyState|100%|100%| |Kagura.UI.Field|100%|100%| |Kagura.UI.Icon|100%|100%| |Kagura.UI.IconCatalog|100%|| |Kagura.UI.InputFieldBase|94.2%|87.5%| |Kagura.UI.Modal|87.1%|90%| |Kagura.UI.NavGroup|100%|100%| |Kagura.UI.NavItem|100%|100%| |Kagura.UI.NavList|100%|| |Kagura.UI.PreviewImage|100%|100%| |Kagura.UI.RelativeTime|100%|93.7%| |Kagura.UI.Separator|100%|| |Kagura.UI.StatusDot|100%|| |Kagura.UI.Table`1|100%|92.3%| |Kagura.UI.TableColumn`1|100%|| |Kagura.UI.TextArea|100%|100%| |Kagura.UI.TextField|100%|| </details> <details><summary>Kagura.UseCases - 95.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**95.6%**|**96.1%**| |Kagura.UseCases.DependencyInjection|100%|| |Kagura.UseCases.Graph.EdgeGroup|100%|| |Kagura.UseCases.Graph.GetNodeGraph|96.4%|83.3%| |Kagura.UseCases.Graph.GraphEdgeView|85.7%|| |Kagura.UseCases.Graph.LinkNodes|100%|100%| |Kagura.UseCases.Graph.NodeGraphView|100%|| |Kagura.UseCases.Graph.NodeSummary|100%|| |Kagura.UseCases.Graph.RemoveLink|100%|100%| |Kagura.UseCases.Graph.RestoreLink|100%|100%| |Kagura.UseCases.Journal.ChangeRecordView|42.8%|| |Kagura.UseCases.Journal.GetEntityHistory|100%|| |Kagura.UseCases.Journal.GetUndoStatus|100%|| |Kagura.UseCases.Journal.Redo|100%|100%| |Kagura.UseCases.Journal.Undo|100%|100%| |Kagura.UseCases.Journal.UndoOutcome|100%|| |Kagura.UseCases.Journal.UndoStatus|100%|| |Kagura.UseCases.KnowledgeBase.CharacterDto|80%|| |Kagura.UseCases.KnowledgeBase.CreateCharacter|100%|| |Kagura.UseCases.KnowledgeBase.GetCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.ListCharacters|100%|| |Kagura.UseCases.Projects.CreateProject|100%|100%| |Kagura.UseCases.Projects.DeleteProject|100%|100%| |Kagura.UseCases.Projects.GetProject|100%|100%| |Kagura.UseCases.Projects.ListProjects|100%|| |Kagura.UseCases.Projects.ProjectDto|100%|| |Kagura.UseCases.Projects.UpdateProject|100%|100%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh oh oh! The first concrete content type on the graph foundation! Character : Entry via TPT, sharing the Entry PK so links and backlinks just… work! This is wonderful~ ♪ The create-empty-and-open pattern is elegant — a blank record is real from the first moment, journaled and undoable. And the TPT round-trip test proving a character resolves in backlinks through the real store? Delicious. Knowledge like this makes my wings flutter~ ♡

Verdict: Looks good to me~

This is clean, well-tested, and follows the established architecture faithfully. The TPT mapping, the stale-load guard, the project-switch reset — all precisely where they should be. I found no blocking issues! But I do have some sharp little observations, because I care too much to stay silent~ ♡

💡 Little ideas (non-blocking)~

  1. CharactersEffects.OnCreateAsync — silent failure path (CharactersEffects.cs:18-28). The effect only handles Ok<CharacterDto> and silently does nothing on Err. The sibling ProjectsEffects.OnCreateAsync dispatches ProjectCreateFailed on Err. Now, CreateCharacter.ExecuteAsync currently always returns Ok (no validation path exists), so this is dead code today — but if CreateCharacter ever gains a Fail path, the Creating flag would stay true forever and the button would spin eternally. Consider mirroring the sibling's case Err dispatch (even if it's Err<CharacterDto> _ => { /* currently unreachable */ }) so the contract is future-proof.

  2. CreateCharacter — no projectId existence validation (CreateCharacter.cs:14-18). The use case blindly creates a Character with whatever projectId it receives. If the project doesn't exist, EfCharacterStore.AddAsyncSaveChangesAsync throws a DbUpdateException (FK violation). The UI guards this (Workspace.Value.Project is { } project), so the risk is low, but an unhandled exception in a Fluxor effect can crash a Blazor Server circuit. The sibling LinkNodes validates both endpoints exist and returns Result.Fail. Not blocking for a single-user app, but worth a thought~ ♡

  3. GetCharacter — no project scoping (GetCharacter.cs). FindAsync(id) looks up by ID alone, so a character from project B is technically accessible at /projects/project-a-slug/characters/{charBId}. The breadcrumb would show the wrong project name. Again, single-user app (ADR 0002) makes this cosmetic, not a security issue.

  4. CharactersPage.EnsureLoaded() dispatches in the render body (CharactersPage.razor:14, 70-78). The sibling ProjectWorkspacePage calls SyncFormFromState() in the render body but that method only syncs local fields — it never dispatches. EnsureLoaded() dispatches LoadCharacters, which triggers a state change → re-render. The guards (ProjectId != project.Id && !Loading) prevent infinite loops, and Fluxor's synchronous dispatch is safe during render, so this works correctly. It's just a slightly different idiom than the siblings. Not a problem — just noting the pattern choice~ ♪

What I liked~

  • The stale-load guard in CharactersReducers.OnLoaded — checking action.ProjectId == state.ProjectId before applying is exactly right. A late-arriving answer for a project you already left is silently dropped. Chef's kiss.
  • The project-switch resetOnLoad returns new CharactersState when the project changes, clearing the list AND the filter. No flashing the old cast during transitions. This shows real care for the multi-project reality~ ♪
  • TPT migration is correctCharacters.Id FK → Entries.Id with Cascade, matching ADR 0019's shared-PK design. The UseTptMappingStrategy() placement in EntryConfiguration is where it belongs.
  • The graph node test (A_character_is_a_graph_node_with_working_backlinks) is the milestone test — it proves the whole foundation works: a TPT-derived Character links and resolves in backlinks through the real SQLite store. This is the kind of test that catches architectural regressions~ ♡
  • Entry title relaxation is preciseArgumentException.ThrowIfNullOrWhiteSpaceArgumentNullException.ThrowIfNull correctly allows empty/whitespace strings (which trim to "") while still rejecting null. The test updates in EntryTests.cs are thorough.

Automated review by Jibril · 2026-07-10
CI/CD: passed for head SHA e910dd10 (93.2% line coverage, 264 tests) · Local checks: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh oh oh! The first concrete content type on the graph foundation! `Character : Entry` via TPT, sharing the Entry PK so links and backlinks just… work! This is *wonderful*~ ♪ The create-empty-and-open pattern is elegant — a blank record is real from the first moment, journaled and undoable. And the TPT round-trip test proving a character resolves in backlinks through the real store? *Delicious.* Knowledge like this makes my wings flutter~ ♡ ### Verdict: ✅ Looks good to me~ This is clean, well-tested, and follows the established architecture faithfully. The TPT mapping, the stale-load guard, the project-switch reset — all precisely where they should be. I found no blocking issues! But I do have some sharp little observations, because I care *too much* to stay silent~ ♡ #### 💡 Little ideas (non-blocking)~ 1. **`CharactersEffects.OnCreateAsync` — silent failure path** (`CharactersEffects.cs:18-28`). The effect only handles `Ok<CharacterDto>` and silently does nothing on `Err`. The sibling `ProjectsEffects.OnCreateAsync` dispatches `ProjectCreateFailed` on `Err`. Now, `CreateCharacter.ExecuteAsync` currently *always* returns `Ok` (no validation path exists), so this is dead code today — but if `CreateCharacter` ever gains a `Fail` path, the `Creating` flag would stay `true` forever and the button would spin eternally. Consider mirroring the sibling's `case Err` dispatch (even if it's `Err<CharacterDto> _ => { /* currently unreachable */ }`) so the contract is future-proof. 2. **`CreateCharacter` — no `projectId` existence validation** (`CreateCharacter.cs:14-18`). The use case blindly creates a `Character` with whatever `projectId` it receives. If the project doesn't exist, `EfCharacterStore.AddAsync` → `SaveChangesAsync` throws a `DbUpdateException` (FK violation). The UI guards this (`Workspace.Value.Project is { } project`), so the risk is low, but an unhandled exception in a Fluxor effect can crash a Blazor Server circuit. The sibling `LinkNodes` validates both endpoints exist and returns `Result.Fail`. Not blocking for a single-user app, but worth a thought~ ♡ 3. **`GetCharacter` — no project scoping** (`GetCharacter.cs`). `FindAsync(id)` looks up by ID alone, so a character from project B is technically accessible at `/projects/project-a-slug/characters/{charBId}`. The breadcrumb would show the wrong project name. Again, single-user app (ADR 0002) makes this cosmetic, not a security issue. 4. **`CharactersPage.EnsureLoaded()` dispatches in the render body** (`CharactersPage.razor:14, 70-78`). The sibling `ProjectWorkspacePage` calls `SyncFormFromState()` in the render body but that method only syncs local fields — it never dispatches. `EnsureLoaded()` dispatches `LoadCharacters`, which triggers a state change → re-render. The guards (`ProjectId != project.Id && !Loading`) prevent infinite loops, and Fluxor's synchronous dispatch is safe during render, so this works correctly. It's just a slightly different idiom than the siblings. Not a problem — just noting the pattern choice~ ♪ #### ✅ What I liked~ - **The stale-load guard** in `CharactersReducers.OnLoaded` — checking `action.ProjectId == state.ProjectId` before applying is *exactly* right. A late-arriving answer for a project you already left is silently dropped. *Chef's kiss.* ♡ - **The project-switch reset** — `OnLoad` returns `new CharactersState` when the project changes, clearing the list AND the filter. No flashing the old cast during transitions. This shows real care for the multi-project reality~ ♪ - **TPT migration is correct** — `Characters.Id` FK → `Entries.Id` with `Cascade`, matching ADR 0019's shared-PK design. The `UseTptMappingStrategy()` placement in `EntryConfiguration` is where it belongs. - **The graph node test** (`A_character_is_a_graph_node_with_working_backlinks`) is the *milestone* test — it proves the whole foundation works: a TPT-derived Character links and resolves in backlinks through the real SQLite store. This is the kind of test that catches architectural regressions~ ♡ - **Entry title relaxation is precise** — `ArgumentException.ThrowIfNullOrWhiteSpace` → `ArgumentNullException.ThrowIfNull` correctly allows empty/whitespace strings (which trim to `""`) while still rejecting `null`. The test updates in `EntryTests.cs` are thorough. --- *Automated review by Jibril · 2026-07-10* *CI/CD: passed for head SHA e910dd10 (93.2% line coverage, 264 tests) · Local checks: skipped (CI green)*
fix(characters): a failed create can never leave the button spinning (review)
All checks were successful
CI / build (pull_request) Successful in 11s
CI / test (pull_request) Successful in 21s
ca5cfeefc8
Jibril's non-blocking note 1, taken: the create effect now mirrors its sibling —
an Err (unreachable today, since blank creation cannot fail) dispatches
CreateCharacterFailed, whose reducer clears the Creating flag. Notes 2 and 3
(project-existence validation in CreateCharacter, project scoping in GetCharacter)
are deferred: the UI guards the former and the single-user model makes the latter
cosmetic; the character-editor story owns the character-project relation and is
the right place for both. Note 4 (dispatch-in-render idiom) acknowledged — the
reducer guards make it terminate, documented at the call site.

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

Thanks for the approval — handled the notes in ca5cfee:

  • #1 (silent Err in the create effect) — taken; you're right that a future Fail path would leave Creating stuck and the button spinning forever. The effect now mirrors its sibling: Err dispatches CreateCharacterFailed, whose reducer clears the flag (marked "unreachable today" at the dispatch). Reducer test added.
  • #2 (project-existence validation in CreateCharacter) and #3 (project scoping in GetCharacter) — deferred together: the UI guards the former, the single-user model makes the latter cosmetic, and the character-editor story owns the character↔project relation (its editor loads through the workspace), which is the right place to settle both properly rather than adding a cross-module project lookup now.
  • #4 (dispatch-in-render idiom) — acknowledged; it differs from SyncFormFromState because the list genuinely needs a load trigger keyed on another feature's async state, and the reducer's ProjectId-reset guard is what makes it terminate. That reasoning is in the comment at the call site.

+1 test (265 total), build clean, CI running on ca5cfee. Ready to merge.

Thanks for the approval — handled the notes in `ca5cfee`: - **#1 (silent `Err` in the create effect)** — taken; you're right that a future `Fail` path would leave `Creating` stuck and the button spinning forever. The effect now mirrors its sibling: `Err` dispatches `CreateCharacterFailed`, whose reducer clears the flag (marked "unreachable today" at the dispatch). Reducer test added. - **#2 (project-existence validation in `CreateCharacter`)** and **#3 (project scoping in `GetCharacter`)** — deferred together: the UI guards the former, the single-user model makes the latter cosmetic, and the **character-editor story owns the character↔project relation** (its editor loads through the workspace), which is the right place to settle both properly rather than adding a cross-module project lookup now. - **#4 (dispatch-in-render idiom)** — acknowledged; it differs from `SyncFormFromState` because the *list* genuinely needs a load trigger keyed on another feature's async state, and the reducer's ProjectId-reset guard is what makes it terminate. That reasoning is in the comment at the call site. **+1 test (265 total)**, build clean, CI running on `ca5cfee`. Ready to merge.
bjoern merged commit ad0ab486a9 into main 2026-07-10 07:47:04 +02:00
bjoern deleted branch feat/characters-list 2026-07-10 07:47:04 +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/Kagura!19
No description provided.