Characters: the first record type — list, create-empty-and-open, editor stub #19
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/characters-list"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The characters-list story — and with it, the graph foundation's first real payload:
Characteris anEntry-derived node via TPT (theCharacterstable 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 bothEntries(Kind=Character) andCharacters.Domain
nullstill rejected) — the story's create-empty-and-open means records start untitled, and UIs render an italic "(unnamed)". Documented onEntry.Edit; the graph tests updated accordingly.Character : Entryin the KnowledgeBase module (ADR 0017) with no specialized columns yet — the editor story adds them (role, personality, visual data, …), each with its own migration.AddCharactersmigration:Characters.IdFK →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
CharactersPagereplaces 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.CharacterEditorPageis a deliberate stub — it anchors the route (/projects/{slug}/characters/{id:guid}) and the four-level breadcrumb contract (Projects › name › Characters › leaf;WorkspaceShellgainsSectionHref+Leafparams) plus a minimal editor state the character-editor story builds on. Not-found state included.Tests — +18 (264 total)
CharacterCreate, andUndoremoves 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).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
Summary
Summary
Coverage
Kagura.BlazorAdapter - 67.9%
Kagura.Domain - 96.4%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 96.1%
n
Kagura.Kernel - 90%
Kagura.Server - 100%
Kagura.UI - 97.7%
Kagura.UseCases - 95.6%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh oh oh! The first concrete content type on the graph foundation!
Character : Entryvia 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)~
CharactersEffects.OnCreateAsync— silent failure path (CharactersEffects.cs:18-28). The effect only handlesOk<CharacterDto>and silently does nothing onErr. The siblingProjectsEffects.OnCreateAsyncdispatchesProjectCreateFailedonErr. Now,CreateCharacter.ExecuteAsynccurrently always returnsOk(no validation path exists), so this is dead code today — but ifCreateCharacterever gains aFailpath, theCreatingflag would staytrueforever and the button would spin eternally. Consider mirroring the sibling'scase Errdispatch (even if it'sErr<CharacterDto> _ => { /* currently unreachable */ }) so the contract is future-proof.CreateCharacter— noprojectIdexistence validation (CreateCharacter.cs:14-18). The use case blindly creates aCharacterwith whateverprojectIdit receives. If the project doesn't exist,EfCharacterStore.AddAsync→SaveChangesAsyncthrows aDbUpdateException(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 siblingLinkNodesvalidates both endpoints exist and returnsResult.Fail. Not blocking for a single-user app, but worth a thought~ ♡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.CharactersPage.EnsureLoaded()dispatches in the render body (CharactersPage.razor:14, 70-78). The siblingProjectWorkspacePagecallsSyncFormFromState()in the render body but that method only syncs local fields — it never dispatches.EnsureLoaded()dispatchesLoadCharacters, 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~
CharactersReducers.OnLoaded— checkingaction.ProjectId == state.ProjectIdbefore applying is exactly right. A late-arriving answer for a project you already left is silently dropped. Chef's kiss. ♡OnLoadreturnsnew CharactersStatewhen 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~ ♪Characters.IdFK →Entries.IdwithCascade, matching ADR 0019's shared-PK design. TheUseTptMappingStrategy()placement inEntryConfigurationis where it belongs.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~ ♡ArgumentException.ThrowIfNullOrWhiteSpace→ArgumentNullException.ThrowIfNullcorrectly allows empty/whitespace strings (which trim to"") while still rejectingnull. The test updates inEntryTests.csare 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)Thanks for the approval — handled the notes in
ca5cfee:Errin the create effect) — taken; you're right that a futureFailpath would leaveCreatingstuck and the button spinning forever. The effect now mirrors its sibling:ErrdispatchesCreateCharacterFailed, whose reducer clears the flag (marked "unreachable today" at the dispatch). Reducer test added.CreateCharacter) and #3 (project scoping inGetCharacter) — 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.SyncFormFromStatebecause 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.