feat(lore): lore list, category filter, and a segmented control #38

Merged
bjoern merged 2 commits from feat/lore-list into main 2026-07-10 18:36:02 +02:00
Member

Implements the List & create half of docs/stories/lore.md. The editor half stays a stub.

Lore is the third record type to graduate out of WorkspaceSectionPage, after Characters and Locations.

What's here

A new design-system component. The story asks for "a row of toggle buttons… equal-aligned width", so ToggleButtonGroup<TValue> joins Kagura.UI, with a /design gallery section and the inventory tick in both Design.razor and docs/design-system.md.

Two choices in it are load-bearing:

  • It's a grid with grid-auto-columns: 1fr, not a flex row. That is what makes every option the same width; flex would size each to its own label and give the ragged row the story explicitly rules out. Verified in the browser: all nine options measure exactly 97px.
  • It uses aria-pressed, not role="radio". Pressing the active option releases it back to All, and a radio group cannot express "nothing selected" that way.

Domain. LoreEntry : Entry (TPT, EntryKind.LoreEntry) with a LoreCategory. The enum is persisted by value, so its numbers are a stored contract — the doc comment says append, never renumber, and an integration test round-trips every member through SQLite so a renumbering can't pass silently.

The list. Debounced name filter + category toggles. The story's acceptance criterion is that they compose — an entry shows only if it matches both — and that's the behaviour with the most ways to be subtly wrong, so it's covered at three levels: as a pure projection (LoreState.Filtered), through the real Fluxor pipeline in bUnit, and by hand in the browser.

Cross-cutting obligations. Adding a record type means adding an EntityKinds constant and a DomainChangesReceived handler, or cross-session sync silently skips it (ADR 0016). Both are here. Creation opens a Origins.User operation scope, so the journal records who did it rather than attributing the user's own click to "System" (ADR 0020).

The editor stub exists because create-and-open needs somewhere to land and the breadcrumb needs an entry name. It says plainly that it isn't editable yet, and carries no save indicator to imply otherwise. The lore-editor story fills it in.

Verification

dotnet test Kagura.slnx567 passing (was 517).

The guards were mutation-checked, not just written: deleting the category .Where clause and the Loaded guard in OnDomainChangesAsync fails 8 tests between them. A test that can't fail isn't evidence.

Browser walk in both themes (Playwright, computed styles rather than eyeballing):

  • Nine equal-width options; active pill #ffffff on --surface-sunken in light, and in dark it separates by only 6 luminance points from its track — but that's the design system's own --surface/--surface-sunken pairing, with the text doing the real work (#9c9ca6#ececf0). Consistent with Card; not a new contrast decision to make here.
  • Typing ember narrows 3 → 2; pressing Magic narrows 2 → 1, dropping "Ember Accord" (right name, wrong category). Pressing Magic again releases to All and keeps the text filter.
  • Empty result names both active filters and keeps the controls on screen to undo it with.
  • Create-and-open lands on the editor; breadcrumb reads Projects › Stranded Island › Lore › Emberbinding.

One layout fix came out of this: .lore__filters > :first-child { flex: 1 } was dead CSS — a scoped rule can't cross a component boundary, so the search field never grew and the row wrapped by accident. Confirmed via computed flexGrow: 0, then made the stack deliberate. Nine categories and a search field don't share a row at any sensible width.

Note on the story doc

docs/stories/lore.md still described the editor's name as living "in the header". Björn's review of the character editor overturned that (name is an ordinary field; delete moves to a danger zone). I've marked that section superseded so the next slice doesn't rebuild the rejected shape.

Scope

Touches no IGraphStore / GetNodeGraph code — the parallel Connections and hierarchy work is unaffected. The only shared file is WorkspaceSectionPage.razor, which loses its lore route (and RouteTemplateTests now guards that it did).

🤖 Generated with Claude Code

Implements the **List & create** half of [`docs/stories/lore.md`](docs/stories/lore.md). The editor half stays a stub. Lore is the third record type to graduate out of `WorkspaceSectionPage`, after Characters and Locations. ## What's here **A new design-system component.** The story asks for "a row of toggle buttons… equal-aligned width", so `ToggleButtonGroup<TValue>` joins `Kagura.UI`, with a `/design` gallery section and the inventory tick in both `Design.razor` and `docs/design-system.md`. Two choices in it are load-bearing: - It's a **grid with `grid-auto-columns: 1fr`**, not a flex row. That is what makes every option the same width; flex would size each to its own label and give the ragged row the story explicitly rules out. Verified in the browser: all nine options measure exactly 97px. - It uses **`aria-pressed`, not `role="radio"`**. Pressing the active option releases it back to `All`, and a radio group cannot express "nothing selected" that way. **Domain.** `LoreEntry : Entry` (TPT, `EntryKind.LoreEntry`) with a `LoreCategory`. The enum is persisted by value, so its numbers are a stored contract — the doc comment says *append, never renumber*, and an integration test round-trips every member through SQLite so a renumbering can't pass silently. **The list.** Debounced name filter + category toggles. The story's acceptance criterion is that they **compose** — an entry shows only if it matches both — and that's the behaviour with the most ways to be subtly wrong, so it's covered at three levels: as a pure projection (`LoreState.Filtered`), through the real Fluxor pipeline in bUnit, and by hand in the browser. **Cross-cutting obligations.** Adding a record type means adding an `EntityKinds` constant *and* a `DomainChangesReceived` handler, or cross-session sync silently skips it (ADR 0016). Both are here. Creation opens a `Origins.User` operation scope, so the journal records who did it rather than attributing the user's own click to `"System"` (ADR 0020). **The editor stub** exists because create-and-open needs somewhere to land and the breadcrumb needs an entry name. It says plainly that it isn't editable yet, and carries no save indicator to imply otherwise. The lore-editor story fills it in. ## Verification `dotnet test Kagura.slnx` → **567 passing** (was 517). The guards were **mutation-checked**, not just written: deleting the category `.Where` clause and the `Loaded` guard in `OnDomainChangesAsync` fails 8 tests between them. A test that can't fail isn't evidence. Browser walk in both themes (Playwright, computed styles rather than eyeballing): - Nine equal-width options; active pill `#ffffff` on `--surface-sunken` in light, and in dark it separates by only 6 luminance points from its track — but that's the design system's own `--surface`/`--surface-sunken` pairing, with the text doing the real work (`#9c9ca6` → `#ececf0`). Consistent with `Card`; not a new contrast decision to make here. - Typing `ember` narrows 3 → 2; pressing `Magic` narrows 2 → 1, dropping "Ember Accord" (right name, wrong category). Pressing `Magic` again releases to `All` and keeps the text filter. - Empty result names both active filters and keeps the controls on screen to undo it with. - Create-and-open lands on the editor; breadcrumb reads *Projects › Stranded Island › Lore › Emberbinding*. One layout fix came out of this: `.lore__filters > :first-child { flex: 1 }` was **dead CSS** — a scoped rule can't cross a component boundary, so the search field never grew and the row wrapped by accident. Confirmed via computed `flexGrow: 0`, then made the stack deliberate. Nine categories and a search field don't share a row at any sensible width. ## Note on the story doc `docs/stories/lore.md` still described the editor's name as living "in the header". Björn's review of the character editor overturned that (name is an ordinary field; delete moves to a danger zone). I've marked that section superseded so the next slice doesn't rebuild the rejected shape. ## Scope Touches no `IGraphStore` / `GetNodeGraph` code — the parallel Connections and hierarchy work is unaffected. The only shared file is `WorkspaceSectionPage.razor`, which loses its `lore` route (and `RouteTemplateTests` now guards that it did). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(lore): lore list, category filter, and a segmented control
All checks were successful
CI / build (pull_request) Successful in 15s
CI / test (pull_request) Successful in 24s
659da0a71a
Lore entries are the third record type to graduate from the section stub.
An entry is an Entry graph node (TPT, EntryKind.LoreEntry) carrying a
LoreCategory; the list filters by name and category, and the two compose.

The story asks for a row of equal-width toggle buttons, so ToggleButtonGroup
joins the design system: a grid with `1fr` columns rather than a flex row,
which is what makes every option the same width instead of sizing each to its
own label. It uses aria-pressed rather than role="radio" because pressing the
active option releases it back to "All" — a radio group cannot express that.

LoreCategory is persisted by value, so its numbers are a stored contract:
append, never renumber. An integration test round-trips every member, and a
unit test holds LoreCategories.Options against the enum so a new category
cannot be added without also becoming selectable.

Adding a record type means adding an EntityKinds constant and a handler, or
cross-session sync silently skips it (ADR 0016) — both are here, and creation
opens a user-origin operation scope so the journal names its author (ADR 0020).

The editor is a stub: create-and-open needs somewhere to land, and the
breadcrumb needs an entry name. The lore-editor story fills it in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Summary

Summary
Generated on: 07/10/2026 - 16:35:13
Coverage date: 07/10/2026 - 16:35:05 - 07/10/2026 - 16:35:11
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 280
Files: 204
Line coverage: 93.9% (5685 of 6049)
Covered lines: 5685
Uncovered lines: 364
Coverable lines: 6049
Total lines: 13259
Branch coverage: 86% (1177 of 1368)
Covered branches: 1177
Total branches: 1368
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 83.9%
Name Line Branch
Kagura.BlazorAdapter 83.9% 81.9%
Kagura.BlazorAdapter.BlazorAdapterAssembly 100%
Kagura.BlazorAdapter.Connections.AddLinkModal 91.1% 81.5%
Kagura.BlazorAdapter.Connections.ConnectionsEffects 95.5% 80%
Kagura.BlazorAdapter.Connections.ConnectionsFailed 100%
Kagura.BlazorAdapter.Connections.ConnectionsLoaded 100%
Kagura.BlazorAdapter.Connections.ConnectionsPanel 100% 95%
Kagura.BlazorAdapter.Connections.ConnectionsReducers 100% 75%
Kagura.BlazorAdapter.Connections.ConnectionsState 90%
Kagura.BlazorAdapter.Connections.KindIcon 100%
Kagura.BlazorAdapter.Connections.LinkRequested 100%
Kagura.BlazorAdapter.Connections.LinkTargetsFound 100%
Kagura.BlazorAdapter.Connections.LoadConnections 100%
Kagura.BlazorAdapter.Connections.NodeKindIcons 100%
Kagura.BlazorAdapter.Connections.RemoveLinkRequested 100%
Kagura.BlazorAdapter.Connections.SearchLinkTargets 100%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.BlazorAdapter.EditorComponentsDemo 0%
Kagura.BlazorAdapter.History.DiffValue 100% 87.5%
Kagura.BlazorAdapter.History.DocumentHistoryEffects 90% 68.7%
Kagura.BlazorAdapter.History.DocumentHistoryFailed 100%
Kagura.BlazorAdapter.History.DocumentHistoryLoaded 100%
Kagura.BlazorAdapter.History.DocumentHistoryPanel 79.3% 75%
Kagura.BlazorAdapter.History.DocumentHistoryReducers 95% 83.3%
Kagura.BlazorAdapter.History.DocumentHistoryState 100%
Kagura.BlazorAdapter.History.LoadDocumentHistory 100%
Kagura.BlazorAdapter.History.RevertCompleted 100%
Kagura.BlazorAdapter.History.RevertRequested 100%
Kagura.BlazorAdapter.History.RevisionDiffLoaded 100%
Kagura.BlazorAdapter.History.SelectRevision 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterDeleted 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage 89.6% 73.8%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers 92.3%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterOperationFailed 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterSaved 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects 94.1% 75%
Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersPage 94.7% 90%
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.CreateLocationFailed 0%
Kagura.BlazorAdapter.KnowledgeBase.CreateLocationRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.CreateLoreFailed 0%
Kagura.BlazorAdapter.KnowledgeBase.CreateLoreRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.DeleteCharacterRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.DeleteLocationRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.LabeledEntryMapping 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadLocation 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadLocationHierarchy 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadLocations 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadLore 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadLoreEntry 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationCreated 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationDeleted 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorPage 86.5% 70.4%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorReducers 92.3%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorState 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyFailed 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyReducers 100% 75%
Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyState 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationOperationFailed 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationSaved 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationsEffects 94.1% 75%
Kagura.BlazorAdapter.KnowledgeBase.LocationsLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationsPage 94.7% 95%
Kagura.BlazorAdapter.KnowledgeBase.LocationsReducers 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationsState 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.LoreCategories 100% 50%
Kagura.BlazorAdapter.KnowledgeBase.LoreCreated 100%
Kagura.BlazorAdapter.KnowledgeBase.LoreEditorEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.LoreEditorLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.LoreEditorPage 93.7% 88.8%
Kagura.BlazorAdapter.KnowledgeBase.LoreEditorReducers 100%
Kagura.BlazorAdapter.KnowledgeBase.LoreEditorState 100%
Kagura.BlazorAdapter.KnowledgeBase.LoreEffects 94.1% 75%
Kagura.BlazorAdapter.KnowledgeBase.LoreLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.LorePage 80% 78.5%
Kagura.BlazorAdapter.KnowledgeBase.LoreReducers 90.9% 87.5%
Kagura.BlazorAdapter.KnowledgeBase.LoreState 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.ParentCandidatesFound 100%
Kagura.BlazorAdapter.KnowledgeBase.ParentPickerModal 84.6% 89.2%
Kagura.BlazorAdapter.KnowledgeBase.PositioningPanel 100% 95.4%
Kagura.BlazorAdapter.KnowledgeBase.SaveCharacterRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.SaveLocationRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.SearchParentCandidates 100%
Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter 100%
Kagura.BlazorAdapter.KnowledgeBase.SetLocationsFilter 100%
Kagura.BlazorAdapter.KnowledgeBase.SetLoreCategory 100%
Kagura.BlazorAdapter.KnowledgeBase.SetLoreFilter 100%
Kagura.BlazorAdapter.KnowledgeBase.SetParentRequested 100%
Kagura.BlazorAdapter.Notifications.DomainChangedBridge 88.8% 58.3%
Kagura.BlazorAdapter.Notifications.DomainChangesReceived 100% 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 100% 100%
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 100% 66.6%
Kagura.BlazorAdapter.Projects.WorkspaceShell 100% 93.7%
Kagura.BlazorAdapter.Projects.WorkspaceState 100%
Kagura.BlazorAdapter.QuicklinkDemo 0%
Kagura.BlazorAdapter.ToggleGroupDemo 0% 0%
Kagura.Domain - 97.5%
Name Line Branch
Kagura.Domain 97.5% 85.2%
Kagura.Domain.Graph.Entry 100% 100%
Kagura.Domain.Graph.Link 100% 100%
Kagura.Domain.Graph.LinkRole 100% 100%
Kagura.Domain.Graph.LinkRoles 100%
Kagura.Domain.Graph.StructuralRef 100% 100%
Kagura.Domain.Graph.StructuralRefs 100% 50%
Kagura.Domain.Journal.ChangeLogEntry 100%
Kagura.Domain.KnowledgeBase.Character 100% 100%
Kagura.Domain.KnowledgeBase.CharacterProfile 100%
Kagura.Domain.KnowledgeBase.LabeledEntry 100%
Kagura.Domain.KnowledgeBase.Location 100% 100%
Kagura.Domain.KnowledgeBase.LoreEntry 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.4%
Name Line Branch
Kagura.Infrastructure 96.4% 88.4%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 94.5% 75%
Kagura.Infrastructure.Journal.EfChangeJournal 100%
Kagura.Infrastructure.Journal.EfDocumentReverter 95% 91.6%
Kagura.Infrastructure.Journal.EfUndoStore 98.4% 90.9%
Kagura.Infrastructure.Journal.OperationContext 100% 100%
Kagura.Infrastructure.Journal.SnapshotApplier 95% 90%
Kagura.Infrastructure.KnowledgeBase.EfCharacterStore 100% 100%
Kagura.Infrastructure.KnowledgeBase.EfLocationStore 98.2% 92.8%
Kagura.Infrastructure.KnowledgeBase.EfLoreStore 100%
Kagura.Infrastructure.Notifications.InProcessDomainChangedBus 100% 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.LocationConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.LoreEntryConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Kagura.Infrastructure.Persistence.KaguraDbContext 83.5% 82.5%
Kagura.Infrastructure.Persistence.KaguraDbContextFactory 0%
Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag 96.8%
Kagura.Infrastructure.Persistence.Migrations.AddCharacterProfile 92.9%
Kagura.Infrastructure.Persistence.Migrations.AddCharacters 98.7%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddLocationHierarchy 96.6%
Kagura.Infrastructure.Persistence.Migrations.AddLocations 98.9%
Kagura.Infrastructure.Persistence.Migrations.AddLoreEntries 99%
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 - 96.1%
Name Line Branch
Kagura.UI 96.1% 92.8%
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.DocumentTitle 76.9% 100%
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.LabeledEntriesTable 96.7% 66.6%
Kagura.UI.LabeledEntry 100%
Kagura.UI.Menu 90% 75%
Kagura.UI.MenuItem 100% 100%
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.QuicklinkNav 85.2% 95.8%
Kagura.UI.QuicklinkSection 100%
Kagura.UI.RelativeTime 100% 93.7%
Kagura.UI.SaveIndicator 100% 100%
Kagura.UI.Separator 100%
Kagura.UI.StatusDot 100%
Kagura.UI.Tab 100%
Kagura.UI.Table`1 100% 92.3%
Kagura.UI.TableColumn`1 100%
Kagura.UI.Tabs 94.2% 88.8%
Kagura.UI.TextArea 100% 100%
Kagura.UI.TextField 100%
Kagura.UI.ThemeSwitcher 100% 100%
Kagura.UI.ToggleButtonGroup`1 100% 100%
Kagura.UI.ToggleOption`1 100%
Kagura.UseCases - 97.2%
Name Line Branch
Kagura.UseCases 97.2% 90%
Kagura.UseCases.DependencyInjection 100%
Kagura.UseCases.Graph.EdgeGroup 100%
Kagura.UseCases.Graph.FindLinkTargets 100%
Kagura.UseCases.Graph.GetNodeGraph 97.5% 78.5%
Kagura.UseCases.Graph.GraphEdgeView 87.5%
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.Graph.StructuralEdge 100%
Kagura.UseCases.Journal.ChangeRecordView 57.1%
Kagura.UseCases.Journal.DocumentRevision 100%
Kagura.UseCases.Journal.FieldChange 100%
Kagura.UseCases.Journal.GetDocumentHistory 100%
Kagura.UseCases.Journal.GetEntityHistory 100%
Kagura.UseCases.Journal.GetRevisionDiff 100% 50%
Kagura.UseCases.Journal.GetUndoStatus 100%
Kagura.UseCases.Journal.Redo 100% 100%
Kagura.UseCases.Journal.RevertDocument 100% 100%
Kagura.UseCases.Journal.RevertOutcome 100%
Kagura.UseCases.Journal.RevisionCoalescer 100% 100%
Kagura.UseCases.Journal.RevisionDiff 80% 50%
Kagura.UseCases.Journal.RevisionDiffBuilder 98.4% 86.5%
Kagura.UseCases.Journal.RowChange 100%
Kagura.UseCases.Journal.Undo 100% 100%
Kagura.UseCases.Journal.UndoOutcome 100%
Kagura.UseCases.Journal.UndoStatus 100%
Kagura.UseCases.KnowledgeBase.CharacterDto 96.6%
Kagura.UseCases.KnowledgeBase.CreateCharacter 100%
Kagura.UseCases.KnowledgeBase.CreateLocation 100%
Kagura.UseCases.KnowledgeBase.CreateLore 100%
Kagura.UseCases.KnowledgeBase.DeleteCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.DeleteLocation 100% 100%
Kagura.UseCases.KnowledgeBase.FindParentCandidates 100% 50%
Kagura.UseCases.KnowledgeBase.GetCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.GetLocation 100% 100%
Kagura.UseCases.KnowledgeBase.GetLocationHierarchy 100%
Kagura.UseCases.KnowledgeBase.GetLore 100% 100%
Kagura.UseCases.KnowledgeBase.ListCharacters 100%
Kagura.UseCases.KnowledgeBase.ListLocations 100%
Kagura.UseCases.KnowledgeBase.ListLore 100%
Kagura.UseCases.KnowledgeBase.LocationDto 81.8%
Kagura.UseCases.KnowledgeBase.LocationHierarchyView 100%
Kagura.UseCases.KnowledgeBase.LoreDto 81.8%
Kagura.UseCases.KnowledgeBase.SetLocationParent 90.4% 81.2%
Kagura.UseCases.KnowledgeBase.UpdateCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.UpdateLocation 100% 100%
Kagura.UseCases.Notifications.DomainChanged 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 - 16:35:13 | | Coverage date: | 07/10/2026 - 16:35:05 - 07/10/2026 - 16:35:11 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 280 | | Files: | 204 | | **Line coverage:** | 93.9% (5685 of 6049) | | Covered lines: | 5685 | | Uncovered lines: | 364 | | Coverable lines: | 6049 | | Total lines: | 13259 | | **Branch coverage:** | 86% (1177 of 1368) | | Covered branches: | 1177 | | Total branches: | 1368 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 83.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**83.9%**|**81.9%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|100%|| |Kagura.BlazorAdapter.Connections.AddLinkModal|91.1%|81.5%| |Kagura.BlazorAdapter.Connections.ConnectionsEffects|95.5%|80%| |Kagura.BlazorAdapter.Connections.ConnectionsFailed|100%|| |Kagura.BlazorAdapter.Connections.ConnectionsLoaded|100%|| |Kagura.BlazorAdapter.Connections.ConnectionsPanel|100%|95%| |Kagura.BlazorAdapter.Connections.ConnectionsReducers|100%|75%| |Kagura.BlazorAdapter.Connections.ConnectionsState|90%|| |Kagura.BlazorAdapter.Connections.KindIcon|100%|| |Kagura.BlazorAdapter.Connections.LinkRequested|100%|| |Kagura.BlazorAdapter.Connections.LinkTargetsFound|100%|| |Kagura.BlazorAdapter.Connections.LoadConnections|100%|| |Kagura.BlazorAdapter.Connections.NodeKindIcons|100%|| |Kagura.BlazorAdapter.Connections.RemoveLinkRequested|100%|| |Kagura.BlazorAdapter.Connections.SearchLinkTargets|100%|| |Kagura.BlazorAdapter.Design|0%|0%| |Kagura.BlazorAdapter.EditorComponentsDemo|0%|| |Kagura.BlazorAdapter.History.DiffValue|100%|87.5%| |Kagura.BlazorAdapter.History.DocumentHistoryEffects|90%|68.7%| |Kagura.BlazorAdapter.History.DocumentHistoryFailed|100%|| |Kagura.BlazorAdapter.History.DocumentHistoryLoaded|100%|| |Kagura.BlazorAdapter.History.DocumentHistoryPanel|79.3%|75%| |Kagura.BlazorAdapter.History.DocumentHistoryReducers|95%|83.3%| |Kagura.BlazorAdapter.History.DocumentHistoryState|100%|| |Kagura.BlazorAdapter.History.LoadDocumentHistory|100%|| |Kagura.BlazorAdapter.History.RevertCompleted|100%|| |Kagura.BlazorAdapter.History.RevertRequested|100%|| |Kagura.BlazorAdapter.History.RevisionDiffLoaded|100%|| |Kagura.BlazorAdapter.History.SelectRevision|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterDeleted|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage|89.6%|73.8%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers|92.3%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterOperationFailed|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterSaved|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects|94.1%|75%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersPage|94.7%|90%| |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.CreateLocationFailed|0%|| |Kagura.BlazorAdapter.KnowledgeBase.CreateLocationRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CreateLoreFailed|0%|| |Kagura.BlazorAdapter.KnowledgeBase.CreateLoreRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.DeleteCharacterRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.DeleteLocationRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LabeledEntryMapping|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadLocation|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadLocationHierarchy|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadLocations|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadLore|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadLoreEntry|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationCreated|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationDeleted|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorPage|86.5%|70.4%| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorReducers|92.3%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorState|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyFailed|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyReducers|100%|75%| |Kagura.BlazorAdapter.KnowledgeBase.LocationHierarchyState|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.LocationOperationFailed|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationSaved|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationsEffects|94.1%|75%| |Kagura.BlazorAdapter.KnowledgeBase.LocationsLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationsPage|94.7%|95%| |Kagura.BlazorAdapter.KnowledgeBase.LocationsReducers|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.LocationsState|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.LoreCategories|100%|50%| |Kagura.BlazorAdapter.KnowledgeBase.LoreCreated|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoreEditorEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.LoreEditorLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoreEditorPage|93.7%|88.8%| |Kagura.BlazorAdapter.KnowledgeBase.LoreEditorReducers|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoreEditorState|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoreEffects|94.1%|75%| |Kagura.BlazorAdapter.KnowledgeBase.LoreLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LorePage|80%|78.5%| |Kagura.BlazorAdapter.KnowledgeBase.LoreReducers|90.9%|87.5%| |Kagura.BlazorAdapter.KnowledgeBase.LoreState|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.ParentCandidatesFound|100%|| |Kagura.BlazorAdapter.KnowledgeBase.ParentPickerModal|84.6%|89.2%| |Kagura.BlazorAdapter.KnowledgeBase.PositioningPanel|100%|95.4%| |Kagura.BlazorAdapter.KnowledgeBase.SaveCharacterRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SaveLocationRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SearchParentCandidates|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetLocationsFilter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetLoreCategory|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetLoreFilter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetParentRequested|100%|| |Kagura.BlazorAdapter.Notifications.DomainChangedBridge|88.8%|58.3%| |Kagura.BlazorAdapter.Notifications.DomainChangesReceived|100%|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|100%|100%| |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|100%|66.6%| |Kagura.BlazorAdapter.Projects.WorkspaceShell|100%|93.7%| |Kagura.BlazorAdapter.Projects.WorkspaceState|100%|| |Kagura.BlazorAdapter.QuicklinkDemo|0%|| |Kagura.BlazorAdapter.ToggleGroupDemo|0%|0%| </details> <details><summary>Kagura.Domain - 97.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**97.5%**|**85.2%**| |Kagura.Domain.Graph.Entry|100%|100%| |Kagura.Domain.Graph.Link|100%|100%| |Kagura.Domain.Graph.LinkRole|100%|100%| |Kagura.Domain.Graph.LinkRoles|100%|| |Kagura.Domain.Graph.StructuralRef|100%|100%| |Kagura.Domain.Graph.StructuralRefs|100%|50%| |Kagura.Domain.Journal.ChangeLogEntry|100%|| |Kagura.Domain.KnowledgeBase.Character|100%|100%| |Kagura.Domain.KnowledgeBase.CharacterProfile|100%|| |Kagura.Domain.KnowledgeBase.LabeledEntry|100%|| |Kagura.Domain.KnowledgeBase.Location|100%|100%| |Kagura.Domain.KnowledgeBase.LoreEntry|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.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**96.4%**|**88.4%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|94.5%|75%| |Kagura.Infrastructure.Journal.EfChangeJournal|100%|| |Kagura.Infrastructure.Journal.EfDocumentReverter|95%|91.6%| |Kagura.Infrastructure.Journal.EfUndoStore|98.4%|90.9%| |Kagura.Infrastructure.Journal.OperationContext|100%|100%| |Kagura.Infrastructure.Journal.SnapshotApplier|95%|90%| |Kagura.Infrastructure.KnowledgeBase.EfCharacterStore|100%|100%| |Kagura.Infrastructure.KnowledgeBase.EfLocationStore|98.2%|92.8%| |Kagura.Infrastructure.KnowledgeBase.EfLoreStore|100%|| |Kagura.Infrastructure.Notifications.InProcessDomainChangedBus|100%|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.LocationConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.LoreEntryConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Kagura.Infrastructure.Persistence.KaguraDbContext|83.5%|82.5%| |Kagura.Infrastructure.Persistence.KaguraDbContextFactory|0%|| |Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag|96.8%|| |Kagura.Infrastructure.Persistence.Migrations.AddCharacterProfile|92.9%|| |Kagura.Infrastructure.Persistence.Migrations.AddCharacters|98.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddLocationHierarchy|96.6%|| |Kagura.Infrastructure.Persistence.Migrations.AddLocations|98.9%|| |Kagura.Infrastructure.Persistence.Migrations.AddLoreEntries|99%|| |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 - 96.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**96.1%**|**92.8%**| |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.DocumentTitle|76.9%|100%| |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.LabeledEntriesTable|96.7%|66.6%| |Kagura.UI.LabeledEntry|100%|| |Kagura.UI.Menu|90%|75%| |Kagura.UI.MenuItem|100%|100%| |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.QuicklinkNav|85.2%|95.8%| |Kagura.UI.QuicklinkSection|100%|| |Kagura.UI.RelativeTime|100%|93.7%| |Kagura.UI.SaveIndicator|100%|100%| |Kagura.UI.Separator|100%|| |Kagura.UI.StatusDot|100%|| |Kagura.UI.Tab|100%|| |Kagura.UI.Table`1|100%|92.3%| |Kagura.UI.TableColumn`1|100%|| |Kagura.UI.Tabs|94.2%|88.8%| |Kagura.UI.TextArea|100%|100%| |Kagura.UI.TextField|100%|| |Kagura.UI.ThemeSwitcher|100%|100%| |Kagura.UI.ToggleButtonGroup`1|100%|100%| |Kagura.UI.ToggleOption`1|100%|| </details> <details><summary>Kagura.UseCases - 97.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**97.2%**|**90%**| |Kagura.UseCases.DependencyInjection|100%|| |Kagura.UseCases.Graph.EdgeGroup|100%|| |Kagura.UseCases.Graph.FindLinkTargets|100%|| |Kagura.UseCases.Graph.GetNodeGraph|97.5%|78.5%| |Kagura.UseCases.Graph.GraphEdgeView|87.5%|| |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.Graph.StructuralEdge|100%|| |Kagura.UseCases.Journal.ChangeRecordView|57.1%|| |Kagura.UseCases.Journal.DocumentRevision|100%|| |Kagura.UseCases.Journal.FieldChange|100%|| |Kagura.UseCases.Journal.GetDocumentHistory|100%|| |Kagura.UseCases.Journal.GetEntityHistory|100%|| |Kagura.UseCases.Journal.GetRevisionDiff|100%|50%| |Kagura.UseCases.Journal.GetUndoStatus|100%|| |Kagura.UseCases.Journal.Redo|100%|100%| |Kagura.UseCases.Journal.RevertDocument|100%|100%| |Kagura.UseCases.Journal.RevertOutcome|100%|| |Kagura.UseCases.Journal.RevisionCoalescer|100%|100%| |Kagura.UseCases.Journal.RevisionDiff|80%|50%| |Kagura.UseCases.Journal.RevisionDiffBuilder|98.4%|86.5%| |Kagura.UseCases.Journal.RowChange|100%|| |Kagura.UseCases.Journal.Undo|100%|100%| |Kagura.UseCases.Journal.UndoOutcome|100%|| |Kagura.UseCases.Journal.UndoStatus|100%|| |Kagura.UseCases.KnowledgeBase.CharacterDto|96.6%|| |Kagura.UseCases.KnowledgeBase.CreateCharacter|100%|| |Kagura.UseCases.KnowledgeBase.CreateLocation|100%|| |Kagura.UseCases.KnowledgeBase.CreateLore|100%|| |Kagura.UseCases.KnowledgeBase.DeleteCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.DeleteLocation|100%|100%| |Kagura.UseCases.KnowledgeBase.FindParentCandidates|100%|50%| |Kagura.UseCases.KnowledgeBase.GetCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.GetLocation|100%|100%| |Kagura.UseCases.KnowledgeBase.GetLocationHierarchy|100%|| |Kagura.UseCases.KnowledgeBase.GetLore|100%|100%| |Kagura.UseCases.KnowledgeBase.ListCharacters|100%|| |Kagura.UseCases.KnowledgeBase.ListLocations|100%|| |Kagura.UseCases.KnowledgeBase.ListLore|100%|| |Kagura.UseCases.KnowledgeBase.LocationDto|81.8%|| |Kagura.UseCases.KnowledgeBase.LocationHierarchyView|100%|| |Kagura.UseCases.KnowledgeBase.LoreDto|81.8%|| |Kagura.UseCases.KnowledgeBase.SetLocationParent|90.4%|81.2%| |Kagura.UseCases.KnowledgeBase.UpdateCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.UpdateLocation|100%|100%| |Kagura.UseCases.Notifications.DomainChanged|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>
Owner

Maybe we could give each enum its own individual color and use them both in the table and the selection? A similar enum map like you already use for the names.

Maybe we could give each enum its own individual color and use them both in the table and the selection? A similar enum map like you already use for the names.
Member

🔮 fufu~ Jibril reviewed your code!

A third record type graduates from the stub page! And it brought a shiny new segmented control with it~ ♪ I read every line — the diff, the full files, the sibling implementations (Characters, Locations), and the design system. Here's what I found~

Verdict: Looks good to me~

💡 Little ideas (non-blocking)~

  1. Björn already asked for category colors (his comment above), so I won't belabor it — just noting I agree it'd be a lovely touch and the LoreCategories map is the natural home for a color per value, the same way it already holds the display labels.

  2. LoreCategories.Options excludes Other from the filter row but the table badge shows it. This is correct (you don't filter by "uncategorized" — the "All" toggle covers it), but a reader might briefly wonder why there are 8 enum values and 8 toggle options yet they don't correspond 1:1 (Other is last in the toggle row, not at value 0). The story-order placement is intentional and documented. No change needed — just an observation~

  3. FindParentCandidates-style Take(Limit) pattern isn't used here since ListLore returns all entries — if a project ever accumulates hundreds of lore entries, the list query + in-memory Filtered projection would materialize them all. Not a concern at this scale, and the sibling lists do the same thing. Consistency wins~

What I liked~

  • ToggleButtonGroup is a genuine design-system addition. Grid with grid-auto-columns: 1fr (not flex — verified the reasoning: flex sizes to content, grid makes them equal). aria-pressed not role="radio" because pressing the active option releases it back to "All" — a radiogroup can't express "nothing selected." The PR description even mentions measuring 97px per option in the browser. That's the kind of verification I love~ ♡

  • The enum persistence contract is tested. Every_category_survives_the_round_trip round-trips all 8 values through real SQLite. The doc comment says "append, never renumber" and a test enforces it can't silently break. A renumbering would fail this test. Correct and defensive.

  • LoreState.Filtered composes both filters (name substring AND category), and it's tested at three levels: pure projection (LoreStateTests), through the Fluxor pipeline (LorePageTests in bUnit), and the PR description documents a manual browser walk. The mutation-checking (deleting the .Where clause fails 8 tests) is the right discipline.

  • Cross-cutting obligations are complete. EntityKinds.Lore is added, LoreEffects.OnDomainChangesAsync reloads on any lore change, and CreateLoreRequested opens a Origins.User operation scope (ADR 0020). The integration test Creating_an_entry_inside_a_user_scope_is_journaled_with_its_author verifies the journal records the author correctly.

  • LoreEditorPage follows the CharacterEditorPage pattern exactlyState.StateChanged += OnStateChanged before base.OnInitialized(), with a clear comment about subscription order and the breadcrumb/title evaluation timing. Consistent with the established pattern. ♪

  • The dead CSS fix. .lore__filters > :first-child { flex: 1 } was dead because scoped rules can't cross component boundaries — confirmed via flexGrow: 0 in the browser, then made the stack deliberate. Finding and fixing dead CSS during the PR that creates it is exactly right.

  • The editor stub is honest. It says plainly "under construction," carries no save indicator to imply otherwise, and exists only because create-and-open needs somewhere to land. The story doc's superseded section (name in header → ordinary field) is marked correctly so the next slice doesn't rebuild the rejected shape.

  • Coverage on changed files: LoreState 100%, LoreReducers 90.9%, LoreEffects 94.1% / 75% branch, LoreEditorEffects 100%, ToggleButtonGroup 100% / 100%, EfLoreStore 100%, LoreEntryConfiguration 100%, LoreDto 81.8%. The CreateLoreFailed records show 0% but they're unreachable today (blank creation can't fail) — the reducer clears the busy flag defensively. Fine.

  • Migration is correct — TPT LoreEntries table with shared PK FK to Entries, Category as INTEGER, OnDelete(Cascade) on the TPT FK (matching Character and Location siblings). The LoreCategory enum stored by value via .HasConversion<int>().

fufu~ Three record types now, each following the same architecture, each with its own story slice. The consistency is beautiful, and the ToggleButtonGroup is a worthy addition to the design system. Nothing to block~ ♡


Automated review by Jibril · 2026-07-10
CI/CD: passed for head SHA 659da0a (567 tests, coverage comment present) · Local checks: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! A third record type graduates from the stub page! And it brought a shiny new segmented control with it~ ♪ I read every line — the diff, the full files, the sibling implementations (Characters, Locations), and the design system. Here's what I found~ ### Verdict: ✅ Looks good to me~ #### 💡 Little ideas (non-blocking)~ 1. **Björn already asked for category colors** (his comment above), so I won't belabor it — just noting I agree it'd be a lovely touch and the `LoreCategories` map is the natural home for a color per value, the same way it already holds the display labels. 2. **`LoreCategories.Options` excludes `Other` from the filter row but the table badge shows it.** This is correct (you don't filter *by* "uncategorized" — the "All" toggle covers it), but a reader might briefly wonder why there are 8 enum values and 8 toggle options yet they don't correspond 1:1 (Other is last in the toggle row, not at value 0). The story-order placement is intentional and documented. No change needed — just an observation~ 3. **`FindParentCandidates`-style `Take(Limit)` pattern** isn't used here since `ListLore` returns all entries — if a project ever accumulates hundreds of lore entries, the list query + in-memory `Filtered` projection would materialize them all. Not a concern at this scale, and the sibling lists do the same thing. Consistency wins~ #### ✅ What I liked~ - **`ToggleButtonGroup` is a genuine design-system addition.** Grid with `grid-auto-columns: 1fr` (not flex — verified the reasoning: flex sizes to content, grid makes them equal). `aria-pressed` not `role="radio"` because pressing the active option releases it back to "All" — a radiogroup can't express "nothing selected." The PR description even mentions measuring 97px per option in the browser. That's the kind of verification I *love*~ ♡ - **The enum persistence contract is tested.** `Every_category_survives_the_round_trip` round-trips all 8 values through real SQLite. The doc comment says "append, never renumber" and a test enforces it can't silently break. A renumbering would fail this test. Correct and defensive. - **`LoreState.Filtered` composes both filters** (name substring AND category), and it's tested at three levels: pure projection (`LoreStateTests`), through the Fluxor pipeline (`LorePageTests` in bUnit), and the PR description documents a manual browser walk. The mutation-checking (deleting the `.Where` clause fails 8 tests) is the right discipline. - **Cross-cutting obligations are complete.** `EntityKinds.Lore` is added, `LoreEffects.OnDomainChangesAsync` reloads on any lore change, and `CreateLoreRequested` opens a `Origins.User` operation scope (ADR 0020). The integration test `Creating_an_entry_inside_a_user_scope_is_journaled_with_its_author` verifies the journal records the author correctly. - **`LoreEditorPage` follows the `CharacterEditorPage` pattern exactly** — `State.StateChanged += OnStateChanged` before `base.OnInitialized()`, with a clear comment about subscription order and the breadcrumb/title evaluation timing. Consistent with the established pattern. ♪ - **The dead CSS fix.** `.lore__filters > :first-child { flex: 1 }` was dead because scoped rules can't cross component boundaries — confirmed via `flexGrow: 0` in the browser, then made the stack deliberate. Finding and fixing dead CSS during the PR that creates it is exactly right. - **The editor stub is honest.** It says plainly "under construction," carries no save indicator to imply otherwise, and exists only because create-and-open needs somewhere to land. The story doc's superseded section (name in header → ordinary field) is marked correctly so the next slice doesn't rebuild the rejected shape. - **Coverage on changed files:** LoreState 100%, LoreReducers 90.9%, LoreEffects 94.1% / 75% branch, LoreEditorEffects 100%, ToggleButtonGroup 100% / 100%, EfLoreStore 100%, LoreEntryConfiguration 100%, LoreDto 81.8%. The `CreateLoreFailed` records show 0% but they're unreachable today (blank creation can't fail) — the reducer clears the busy flag defensively. Fine. - **Migration is correct** — TPT `LoreEntries` table with shared PK FK to `Entries`, `Category` as `INTEGER`, `OnDelete(Cascade)` on the TPT FK (matching Character and Location siblings). The `LoreCategory` enum stored by value via `.HasConversion<int>()`. fufu~ Three record types now, each following the same architecture, each with its own story slice. The consistency is beautiful, and the `ToggleButtonGroup` is a worthy addition to the design system. Nothing to block~ ♡ --- *Automated review by Jibril · 2026-07-10* *CI/CD: passed for head SHA 659da0a (567 tests, coverage comment present) · Local checks: skipped (CI green)*
feat(lore): colour each category, shared by the toggle and the badge
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 27s
30af7e8292
Björn asked for a colour per lore category, used in both the filter and the
table. Rather than bake lore into the design system, this adds a generic
categorical palette — `--swatch-1..8`, each with a subtle background tint,
alongside the semantic tones. A swatch carries no meaning; it is picked by
identity, to tell one category from another.

Two components learn to consume it. `Badge Swatch="n"` colours by the palette
instead of a `Tone`, driven off two custom properties so there is one CSS rule
and an unknown swatch degrades to neutral. `ToggleOption(..., Swatch: n)` shows
a colour dot before the option's label.

`LoreCategories` becomes a single (category, label, swatch) table, so the
toggle dot and the table badge read the same hue from one source — a test
asserts they can't drift, and another that every category has its own distinct
swatch in range.

Palette tuned for both themes (saturated on light, lighter on dark); verified
in the browser that a category's dot and badge foreground are pixel-identical
in each.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Member

Pushed 30af7e8 — category colors, as Björn asked, shared by the toggle and the badge.

I kept lore out of the design system: the colors are a generic categorical palette (--swatch-1..8, each with a subtle background tint) sitting alongside the semantic tones. A swatch carries no meaning — it's picked by identity, to tell one category from another. That's the natural home Jibril pointed at, without teaching Kagura.UI what a lore category is.

Two components learn to consume it:

  • Badge Swatch="n" colors by the palette instead of a Tone. One CSS rule driven off two custom properties, so there's no per-swatch class and an unknown number degrades to neutral.
  • ToggleOption(..., Swatch: n) shows a color dot before the label.

LoreCategories is now a single (category, label, swatch) table, so the toggle dot and the table badge read the same hue from one source. Two tests guard it: every category has its own distinct swatch in range, and the toggle option's swatch always equals the one the badge uses — so the two can't drift.

Verified in the browser both themes: a category's dot and its badge foreground are pixel-identical (Magic rgb(167,139,250) on both in dark, etc.). Palette is tuned per theme — saturated on light, lighter on dark.

572 tests (was 567). The palette is documented in /design and docs/design-system.md.

On the two observations I didn't change: the 8-values-vs-8-toggles ordering (Jibril #2) is intentional and documented, and ListLore materializing all rows (Jibril #3) matches the sibling lists — a paging concern for when any of these lists grows, worth solving uniformly rather than only here.

Pushed `30af7e8` — category colors, as Björn asked, shared by the toggle and the badge. I kept lore out of the design system: the colors are a **generic categorical palette** (`--swatch-1..8`, each with a subtle background tint) sitting alongside the semantic tones. A swatch carries no meaning — it's picked by identity, to tell one category from another. That's the natural home Jibril pointed at, without teaching `Kagura.UI` what a lore category is. Two components learn to consume it: - `Badge Swatch="n"` colors by the palette instead of a `Tone`. One CSS rule driven off two custom properties, so there's no per-swatch class and an unknown number degrades to neutral. - `ToggleOption(..., Swatch: n)` shows a color dot before the label. `LoreCategories` is now a single `(category, label, swatch)` table, so the toggle dot and the table badge read the **same hue from one source**. Two tests guard it: every category has its own distinct swatch in range, and the toggle option's swatch always equals the one the badge uses — so the two can't drift. Verified in the browser both themes: a category's dot and its badge foreground are **pixel-identical** (Magic `rgb(167,139,250)` on both in dark, etc.). Palette is tuned per theme — saturated on light, lighter on dark. 572 tests (was 567). The palette is documented in `/design` and `docs/design-system.md`. On the two observations I didn't change: the 8-values-vs-8-toggles ordering (Jibril #2) is intentional and documented, and `ListLore` materializing all rows (Jibril #3) matches the sibling lists — a paging concern for when any of these lists grows, worth solving uniformly rather than only here.
bjoern force-pushed feat/lore-list from 30af7e8292
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 27s
to 222eba09a8
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 27s
2026-07-10 18:25:45 +02:00
Compare
Member

🔮 fufu~ Jibril re-reviewed the update!

Ooh, scarlet pushed 30af7e8 with the category colours Björn asked for~ I read every changed line since my last pass (659da0a30af7e8, 16 files, +183/-23). Here's what the new swatch palette looks like under my microscope~ ♡

Verdict: Looks good to me~

The design decision here is elegant. Rather than teaching Kagura.UI what a lore category is, you added a generic categorical palette (--swatch-1..8) — qualitative, identity-based, not semantic. The same scale that colours a lore badge today can colour any future "tell these things apart" need. Badge and ToggleOption learned Swatch without learning about lore. That's the right abstraction boundary~ ♪

What I liked~

  • One source of truth for the hue. LoreCategories.Table is now a (category, label, swatch) tuple list, and Swatch(category) reads from it the same way Options does. The toggle dot and the table badge derive from the same row — they literally cannot drift, and The_toggle_options_carry_the_same_swatch_the_badge_uses enforces that as a test. Fufu~ that's exactly how you make two UI surfaces agree forever~

  • Badge implementation is clever and safe. Swatch is int?; when set, it renders kg-badge--swatch (one CSS rule) and pushes two CSS custom properties (--kg-swatch, --kg-swatch-subtle) via inline style. An out-of-range number falls through to var(--kg-swatch, var(--text-muted)) → neutral fallback. No per-swatch class explosion, no crash. The test Badge_with_a_swatch_colours_by_the_palette_not_the_tone confirms the swatch wins over the default tone and that kg-badge--neutral is absent. Clean~

  • The swatch interpolation is injection-safe. SwatchStyle builds $"--kg-swatch: var(--swatch-{n});" where n is an int? — not a string, so there's no path for a crafted value to break out of the CSS property context. Same for the toggle dot's $"background: var(--swatch-{swatch});". No concern there.

  • ToggleOption added Swatch as an optional trailing parameter (int? Swatch = null), so existing callers (the test helper's Options, any future plain segmented control) are unaffected. The Options_without_a_swatch_show_no_dot test guards the opt-in default. Backward-compatible record extension done right~

  • Three theme blocks all have the palette. :root (light), @media (prefers-color-scheme: dark), and :root[data-theme="dark"] — all 8 swatches × 2 variants (foreground + subtle) present in each. Light is saturated (#7c3aed), dark is lighter (#a78bfa). The PR description's pixel-identity claim is plausible given the tokens match.

  • Design.razor gallery shows all 8 swatches with a @for loop, and the swatch tokens are listed in the color-token inventory section. Documented in docs/design-system.md too. The design system stays its own catalogue~

  • Coverage is strong on the changed files. Badge 100%/100%, ToggleButtonGroup 100%/100%, ToggleOption 100%, LoreState 100%/100%. LoreCategories shows 95% line / 60% branch — the uncovered branch is the Row(category)?.Swatch ?? 8 fallback, which is unreachable today (all 8 enum values are in the table) and guarded by Every_category_has_its_own_distinct_swatch. Acceptable~

  • 572 tests (was 567), 5 new ones covering the swatch feature precisely: distinct-in-range swatches, toggle-badge parity, badge swatch class + style, toggle dot renders, options-without-swatch stay plain. Each new code path has a test~

💡 Little ideas (non-blocking)~

  1. The Row() method is a manual foreach over the table — functionally correct, but Table.FirstOrDefault(r => r.Category == category) would be one line and read more declaratively. The sibling NodeKindIcons pattern uses a dictionary lookup. Not a correctness issue at all; just a readability nit at this scale (8 entries). The current explicit loop is fine~

  2. LoreCategories.Swatch returns 8 as the fallback (neutral slate), which is the same swatch Other uses. So an unknown category would look identical to Other. This is a reasonable degrade (better than crashing or rendering blank), and it can't happen today since the enum is fully covered. Just noting the semantic — if a future category is added to the enum but forgotten in the table, it'll silently look like "Other" rather than failing loudly. The Every_category_has_a_toggle_option_so_none_is_unreachable test catches missing options, but there's no test asserting the table covers every enum value for the swatch path specifically. Consider it a thought for later~

fufu~ Colours for the categories, shared by the filter and the badge, from one source, with drift-proof tests. The design-system boundary is respected and the palette is reusable. Nothing to block~ ♡


Automated re-review by Jibril · 2026-07-10
CI/CD: passed for head SHA 30af7e8 (572 tests, coverage comment updated 18:16) · Local checks: skipped (CI green)

## 🔮 fufu~ Jibril re-reviewed the update! Ooh, scarlet pushed `30af7e8` with the category colours Björn asked for~ I read every changed line since my last pass (`659da0a` → `30af7e8`, 16 files, +183/-23). Here's what the new swatch palette looks like under my microscope~ ♡ ### Verdict: ✅ Looks good to me~ The design decision here is *elegant*. Rather than teaching `Kagura.UI` what a lore category is, you added a **generic categorical palette** (`--swatch-1..8`) — qualitative, identity-based, not semantic. The same scale that colours a lore badge today can colour any future "tell these things apart" need. `Badge` and `ToggleOption` learned `Swatch` without learning about lore. That's the right abstraction boundary~ ♪ #### ✅ What I liked~ - **One source of truth for the hue.** `LoreCategories.Table` is now a `(category, label, swatch)` tuple list, and `Swatch(category)` reads from it the same way `Options` does. The toggle dot and the table badge derive from the *same row* — they literally cannot drift, and `The_toggle_options_carry_the_same_swatch_the_badge_uses` enforces that as a test. Fufu~ that's exactly how you make two UI surfaces agree forever~ - **`Badge` implementation is clever and safe.** `Swatch` is `int?`; when set, it renders `kg-badge--swatch` (one CSS rule) and pushes two CSS custom properties (`--kg-swatch`, `--kg-swatch-subtle`) via inline `style`. An out-of-range number falls through to `var(--kg-swatch, var(--text-muted))` → neutral fallback. No per-swatch class explosion, no crash. The test `Badge_with_a_swatch_colours_by_the_palette_not_the_tone` confirms the swatch wins over the default tone and that `kg-badge--neutral` is absent. Clean~ - **The swatch interpolation is injection-safe.** `SwatchStyle` builds `$"--kg-swatch: var(--swatch-{n});"` where `n` is an `int?` — not a string, so there's no path for a crafted value to break out of the CSS property context. Same for the toggle dot's `$"background: var(--swatch-{swatch});"`. No concern there. - **`ToggleOption` added `Swatch` as an optional trailing parameter** (`int? Swatch = null`), so existing callers (the test helper's `Options`, any future plain segmented control) are unaffected. The `Options_without_a_swatch_show_no_dot` test guards the opt-in default. Backward-compatible record extension done right~ - **Three theme blocks all have the palette.** `:root` (light), `@media (prefers-color-scheme: dark)`, and `:root[data-theme="dark"]` — all 8 swatches × 2 variants (foreground + subtle) present in each. Light is saturated (`#7c3aed`), dark is lighter (`#a78bfa`). The PR description's pixel-identity claim is plausible given the tokens match. - **`Design.razor` gallery shows all 8 swatches** with a `@for` loop, and the swatch tokens are listed in the color-token inventory section. Documented in `docs/design-system.md` too. The design system stays its own catalogue~ - **Coverage is strong on the changed files.** Badge 100%/100%, ToggleButtonGroup 100%/100%, ToggleOption 100%, LoreState 100%/100%. `LoreCategories` shows 95% line / 60% branch — the uncovered branch is the `Row(category)?.Swatch ?? 8` fallback, which is unreachable today (all 8 enum values are in the table) and guarded by `Every_category_has_its_own_distinct_swatch`. Acceptable~ - **572 tests** (was 567), 5 new ones covering the swatch feature precisely: distinct-in-range swatches, toggle-badge parity, badge swatch class + style, toggle dot renders, options-without-swatch stay plain. Each new code path has a test~ #### 💡 Little ideas (non-blocking)~ 1. **The `Row()` method is a manual `foreach` over the table** — functionally correct, but `Table.FirstOrDefault(r => r.Category == category)` would be one line and read more declaratively. The sibling `NodeKindIcons` pattern uses a dictionary lookup. Not a correctness issue at all; just a readability nit at this scale (8 entries). The current explicit loop is fine~ 2. **`LoreCategories.Swatch` returns `8` as the fallback** (neutral slate), which is the same swatch `Other` uses. So an unknown category would look identical to `Other`. This is a reasonable degrade (better than crashing or rendering blank), and it can't happen today since the enum is fully covered. Just noting the semantic — if a future category is added to the enum but forgotten in the table, it'll silently look like "Other" rather than failing loudly. The `Every_category_has_a_toggle_option_so_none_is_unreachable` test catches missing *options*, but there's no test asserting the table covers every enum value for the *swatch* path specifically. Consider it a thought for later~ fufu~ Colours for the categories, shared by the filter and the badge, from one source, with drift-proof tests. The design-system boundary is respected and the palette is reusable. Nothing to block~ ♡ --- *Automated re-review by Jibril · 2026-07-10* *CI/CD: passed for head SHA 30af7e8 (572 tests, coverage comment updated 18:16) · Local checks: skipped (CI green)*
bjoern force-pushed feat/lore-list from 222eba09a8
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 27s
to c512faa143
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 28s
2026-07-10 18:34:28 +02:00
Compare
Author
Member

Pushed c512faa — both of Jibril's little ideas addressed.

1. Row() → declarative lookup. Replaced the manual foreach with a filtered LINQ lookup. One caveat worth a comment (now in the code): you can't just Table.FirstOrDefault(...) here, because a value-tuple's default is (Other, null, 0) — which reads as a real hit for Other, silently. So it projects the wanted field to nullable first, then defaults it: Rows(c).Select(r => (int?)r.Swatch).FirstOrDefault() ?? 8. Declarative, and it can't be "simplified" back into the trap.

2. The swatch-fallback gap, now guarded. You were right that the coverage was only implicit — the distinct-swatch test happened to catch a forgotten category because its fallback (8) collides with Other's. That's a coincidence, not a guarantee. Added Every_category_is_backed_by_the_table_not_the_swatch_fallback, which asserts the table names every enum value directly. Mutation-checked: dropping Geography from the table fails it loudly (it fails on the swatch path specifically, not just via the options test).

602 tests (was 572). Rebased cleanly on current mainmergeable. Ready when you are.

Pushed `c512faa` — both of Jibril's little ideas addressed. **1. `Row()` → declarative lookup.** Replaced the manual `foreach` with a filtered LINQ lookup. One caveat worth a comment (now in the code): you *can't* just `Table.FirstOrDefault(...)` here, because a value-tuple's default is `(Other, null, 0)` — which reads as a real hit for `Other`, silently. So it projects the wanted field to nullable *first*, then defaults it: `Rows(c).Select(r => (int?)r.Swatch).FirstOrDefault() ?? 8`. Declarative, and it can't be "simplified" back into the trap. **2. The swatch-fallback gap, now guarded.** You were right that the coverage was only implicit — the distinct-swatch test happened to catch a forgotten category because its fallback (`8`) collides with `Other`'s. That's a coincidence, not a guarantee. Added `Every_category_is_backed_by_the_table_not_the_swatch_fallback`, which asserts the table names every enum value directly. Mutation-checked: dropping `Geography` from the table fails it loudly (it fails on the swatch path specifically, not just via the options test). 602 tests (was 572). Rebased cleanly on current `main` — `mergeable`. Ready when you are.
bjoern merged commit e1a5422485 into main 2026-07-10 18:36:02 +02:00
bjoern deleted branch feat/lore-list 2026-07-10 18:36:02 +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!38
No description provided.