Locations: the second record type — and the graph's second node kind #30

Merged
bjoern merged 4 commits from feat/locations into main 2026-07-10 14:49:45 +02:00
Member

Locations, mirroring how characters landed: Location : Entry (TPT), a store port and EF adapter, CreateLocation / ListLocations / GetLocation, a filtered list with create-empty-and-open, and an editor stub the location-editor story will fill in.

Why this, before the Connections panel

The list is not the point. The graph now has a second node type.

home_of needs a character at one end and a location at the other. Until now CharactersTests had to fake the far end with a bare Entry, and its comment said as much:

// A generic Entry node on the other end — the location type doesn't exist yet.

LocationsTests links the real pair and asserts what ADR 0019 actually promises: one row, two labels. Read from the character it is home_of; read from the location it is residents. Nobody created it twice. That is precisely what the Connections panel renders — and it could not have been demonstrated before this PR. A Connections panel built last week would have been architecturally correct and entirely undemonstrable.

The History panel needed nothing

The editor stub mounts DocumentHistoryPanel and that is the whole diff — no changes to the panel, the slice, or the journal. A location is a document like any other, so it took only a DocumentId. That was the design claim in #28; this is the first evidence it holds on a record type it was never written for.

Two things the mirror surfaced

Locations had to graduate out of WorkspaceSectionPage. Two components claiming /projects/{slug}/locations compiles cleanly and throws only when a visitor opens the page. RouteTemplateTests now fails on any duplicate route, and on the stub keeping a route that has its own page — the exact mistake the next record type will make. Mutation-checked: re-adding the route fails both tests.

EntityKinds.Location and a feature effect, or the section silently skips cross-session sync — as that file's own remark warns. Both are here, and a test pins that a character's change does not reload the locations list: they share the Entries table via TPT, and only the concrete CLR type tells them apart.

Scope

Location lands bare, exactly as Character did. ParentLocationId, base tags, and background variants belong to the location-editor and location-backgrounds stories, each with its own migration. I did not build them ahead of their callers.

I also did not touch CharacterEditorPage or anything else in the character-editor slice, which is still in flight.

Verification

423 tests green (was 412 on main), release build clean under warnings-as-errors.

The integration tests run against real on-disk SQLite: blank creation persists, journals as a Location create, and undoes; locations are project-scoped; characters and locations stay distinct records despite sharing the Entries table; the cross-kind home_of link reads correctly from both ends; and a location has its own document history.

Driven in a browser on a fresh database: the AddLocations migration applies at startup, the side menu reaches a real page rather than the stub, creating lands on the blank record's editor, its History reads created the location · User · just now, the list shows it with the placeholder preview, and the sibling stub routes still resolve.

Next

Connections panel, now that it has something to connect. One gap to fix when it lands, noted while reading: GetNodeGraph reads only Link rows, but ADR 0019 requires backlink views to union the typed structural refs too — a scene's primary location is a typed FK, not a link, so "scenes set here" would miss the primary setting. Nothing owns such a ref yet, so nothing is broken today, but the union has to arrive with the first one.

Locations, mirroring how characters landed: `Location : Entry` (TPT), a store port and EF adapter, `CreateLocation` / `ListLocations` / `GetLocation`, a filtered list with create-empty-and-open, and an editor stub the [location-editor](docs/stories/location-editor.md) story will fill in. ### Why this, before the Connections panel The list is not the point. **The graph now has a second node type.** `home_of` needs a character at one end and a location at the other. Until now `CharactersTests` had to fake the far end with a bare `Entry`, and its comment said as much: ```csharp // A generic Entry node on the other end — the location type doesn't exist yet. ``` `LocationsTests` links the real pair and asserts what ADR 0019 actually promises: **one row, two labels.** Read from the character it is `home_of`; read from the location it is `residents`. Nobody created it twice. That is precisely what the Connections panel renders — and it could not have been demonstrated before this PR. A Connections panel built last week would have been architecturally correct and entirely undemonstrable. ### The History panel needed nothing The editor stub mounts `DocumentHistoryPanel` and that is the whole diff — no changes to the panel, the slice, or the journal. A location is a document like any other, so it took only a `DocumentId`. That was the design claim in #28; this is the first evidence it holds on a record type it was never written for. ### Two things the mirror surfaced **Locations had to graduate out of `WorkspaceSectionPage`.** Two components claiming `/projects/{slug}/locations` compiles cleanly and throws only when a visitor opens the page. `RouteTemplateTests` now fails on any duplicate route, and on the stub keeping a route that has its own page — the exact mistake the next record type will make. Mutation-checked: re-adding the route fails both tests. **`EntityKinds.Location` *and* a feature effect, or the section silently skips cross-session sync** — as that file's own remark warns. Both are here, and a test pins that a *character's* change does not reload the locations list: they share the `Entries` table via TPT, and only the concrete CLR type tells them apart. ### Scope `Location` lands bare, exactly as `Character` did. `ParentLocationId`, base tags, and background variants belong to the location-editor and location-backgrounds stories, each with its own migration. I did not build them ahead of their callers. I also did not touch `CharacterEditorPage` or anything else in the character-editor slice, which is still in flight. ### Verification **423 tests green** (was 412 on main), release build clean under warnings-as-errors. The integration tests run against real on-disk SQLite: blank creation persists, journals as a `Location` create, and undoes; locations are project-scoped; characters and locations stay distinct records despite sharing the `Entries` table; the cross-kind `home_of` link reads correctly from both ends; and a location has its own document history. Driven in a browser on a **fresh database**: the `AddLocations` migration applies at startup, the side menu reaches a real page rather than the stub, creating lands on the blank record's editor, its History reads `created the location · User · just now`, the list shows it with the placeholder preview, and the sibling stub routes still resolve. ### Next Connections panel, now that it has something to connect. One gap to fix when it lands, noted while reading: `GetNodeGraph` reads **only `Link` rows**, but ADR 0019 requires backlink views to union the *typed structural refs* too — a scene's primary location is a typed FK, not a link, so "scenes set here" would miss the primary setting. Nothing owns such a ref yet, so nothing is broken today, but the union has to arrive with the first one.
feat(locations): the second record type — list, create-empty-and-open, editor stub
All checks were successful
CI / build (pull_request) Successful in 15s
CI / test (pull_request) Successful in 24s
1bd81abe5e
Locations, mirroring how characters landed: Location : Entry (TPT), a store port
and EF adapter, CreateLocation / ListLocations / GetLocation, a filtered list
with create-empty-and-open, and an editor stub the location-editor story will
fill in.

The point is not the list. It is that the graph now has a second node type.
`home_of` needs a character at one end and a location at the other; until now
CharactersTests had to fake the far end with a bare Entry, and its comment said
so. LocationsTests links the real pair and asserts what ADR 0019 promises: one
row, read from the character as "home_of", read from the location as
"residents". Nobody created it twice. That is what the Connections panel
renders, and it could not have been demonstrated before this.

The editor stub already carries the shared History panel. A location is a
document like any other, so the panel took nothing but its id — no changes to
it, none to the journal. That is the whole reason it was built knowing only a
DocumentId (ADR 0022), and this is the first evidence the claim holds.

Two things the mirror surfaced:

- Locations had to graduate out of WorkspaceSectionPage. Two components claiming
  /projects/{slug}/locations compiles cleanly and throws only when a visitor
  opens the page. RouteTemplateTests now fails on any duplicate route, and on
  the stub keeping a route that has its own page — the exact mistake the next
  record type will make. Mutation-checked: re-adding the route fails both.

- EntityKinds.Location and a feature effect, together, or the section silently
  skips cross-session sync — as that file's own remark warns. Both are here, and
  a test pins that a character's change does not reload the locations list:
  they share the Entry table, and only the concrete CLR type tells them apart.

Verified in a browser on a fresh database: the migration applies, the side menu
reaches a real page (not the stub), creating lands on the blank record's editor,
its History reads "created the location · User · just now", the list shows it
with the placeholder preview, and the sibling stub routes still resolve.

423 tests green, release build clean.

Summary

Summary
Generated on: 07/10/2026 - 12:49:09
Coverage date: 07/10/2026 - 12:49:02 - 07/10/2026 - 12:49:07
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 210
Files: 167
Line coverage: 93.5% (4358 of 4660)
Covered lines: 4358
Uncovered lines: 302
Coverable lines: 4660
Total lines: 10096
Branch coverage: 86.2% (868 of 1006)
Covered branches: 868
Total branches: 1006
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 79%
Name Line Branch
Kagura.BlazorAdapter 79% 80.5%
Kagura.BlazorAdapter.BlazorAdapterAssembly 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.5% 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.DeleteCharacterRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.LabeledEntryMapping 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadLocation 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadLocations 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationCreated 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorPage 92.8% 77.2%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorReducers 100%
Kagura.BlazorAdapter.KnowledgeBase.LocationEditorState 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.SaveCharacterRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter 100%
Kagura.BlazorAdapter.KnowledgeBase.SetLocationsFilter 100%
Kagura.BlazorAdapter.Notifications.DomainChangedBridge 88.8% 58.3%
Kagura.BlazorAdapter.Notifications.DomainChangesReceived 75% 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.Domain - 97%
Name Line Branch
Kagura.Domain 97% 85%
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% 100%
Kagura.Domain.KnowledgeBase.CharacterProfile 100%
Kagura.Domain.KnowledgeBase.LabeledEntry 100%
Kagura.Domain.KnowledgeBase.Location 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%
Name Line Branch
Kagura.Infrastructure 96% 88.1%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 95.5% 66.6%
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 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.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.AddLocations 98.9%
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 - 95.9%
Name Line Branch
Kagura.UI 95.9% 92.1%
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.UseCases - 97.4%
Name Line Branch
Kagura.UseCases 97.4% 91.7%
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 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.DeleteCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.GetCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.GetLocation 100% 100%
Kagura.UseCases.KnowledgeBase.ListCharacters 100%
Kagura.UseCases.KnowledgeBase.ListLocations 100%
Kagura.UseCases.KnowledgeBase.LocationDto 80%
Kagura.UseCases.KnowledgeBase.UpdateCharacter 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 - 12:49:09 | | Coverage date: | 07/10/2026 - 12:49:02 - 07/10/2026 - 12:49:07 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 210 | | Files: | 167 | | **Line coverage:** | 93.5% (4358 of 4660) | | Covered lines: | 4358 | | Uncovered lines: | 302 | | Coverable lines: | 4660 | | Total lines: | 10096 | | **Branch coverage:** | 86.2% (868 of 1006) | | Covered branches: | 868 | | Total branches: | 1006 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 79%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**79%**|**80.5%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|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.5%|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.DeleteCharacterRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LabeledEntryMapping|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadLocation|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadLocations|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationCreated|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorPage|92.8%|77.2%| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorReducers|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LocationEditorState|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.SaveCharacterRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetLocationsFilter|100%|| |Kagura.BlazorAdapter.Notifications.DomainChangedBridge|88.8%|58.3%| |Kagura.BlazorAdapter.Notifications.DomainChangesReceived|75%|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%|| </details> <details><summary>Kagura.Domain - 97%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**97%**|**85%**| |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%|100%| |Kagura.Domain.KnowledgeBase.CharacterProfile|100%|| |Kagura.Domain.KnowledgeBase.LabeledEntry|100%|| |Kagura.Domain.KnowledgeBase.Location|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%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**96%**|**88.1%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|95.5%|66.6%| |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|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.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.AddLocations|98.9%|| |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 - 95.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**95.9%**|**92.1%**| |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%| </details> <details><summary>Kagura.UseCases - 97.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**97.4%**|**91.7%**| |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|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.DeleteCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.GetCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.GetLocation|100%|100%| |Kagura.UseCases.KnowledgeBase.ListCharacters|100%|| |Kagura.UseCases.KnowledgeBase.ListLocations|100%|| |Kagura.UseCases.KnowledgeBase.LocationDto|80%|| |Kagura.UseCases.KnowledgeBase.UpdateCharacter|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>
docs(graph): write the backlink gap into the repo, and make it fail loudly
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 23s
96c2242299
A PR description is a message, not documentation — it scrolls away and nobody
reads it two months later. The one knowingly-unfinished part of ADR 0019 now
lives where the person who trips over it will be standing.

ADR 0019 requires backlink views to union the typed structural refs with the
generic Link edges. GetNodeGraph reads Link rows only. Nothing is wrong yet,
because no entity owns such a ref: the graph's node types are Character and
Location, and both link generically. The day one appears — Scene.PrimaryLocationId
or Location.ParentLocationId — "scenes set in this location" starts omitting the
scenes actually set there, and no build, test, or page render says a word.

So four places, in descending order of who will actually read them:

- BacklinkCompletenessTests reads the EF model and fails the moment any entity
  declares a foreign key to a graph node (excluding Link's own endpoints and a
  TPT subtype's PK). It names the reference and says what to do. Verified by
  adding the exact ref that is coming, as a shadow FK: it fails with
  "Location.ParentLocationId → Location" and the instructions.

  It deliberately does not migrate a database. Adding a mapped reference also
  desynchronizes the model from the migrations, and EF's pending-changes warning
  would throw first — burying this message under an unrelated one at precisely
  the moment it needs to be read. Found that by running the mutation check.

- GetNodeGraph's remarks: what is missing, the canonical example, why it is
  silent, and where the union belongs. IGraphStore.LinksTouchingAsync points back.

- ADR 0019 gains an "Implementation status" section — the decision is unchanged;
  what is built, what is not, and why the gap is dangerous.

- The scene-editor and location-editor stories each carry a warning, because each
  is the story that would introduce the first typed ref, and each names its own.

424 tests green, release build clean.
Author
Member

Fair point that the backlink gap should not live in a PR description — those scroll away and nobody reads them in six months. Moved into the repo, in 96c2242.

Four places, in descending order of who will actually read them:

1. A tripwire test. BacklinkCompletenessTests reads the EF model and fails the moment any entity declares a foreign key to a graph node — excluding Link's own endpoints and a TPT subtype's PK, which are not associations. It names the offending reference and says what to do:

A typed structural reference between graph nodes now exists:
  Location.ParentLocationId → Location

ADR 0019 requires backlink views to union these with the generic Link edges — otherwise
GetNodeGraph returns a graph that looks complete and is not (see its remarks, and the
Implementation status section of docs/adr/0019-knowledge-graph-links.md).

Extend IGraphStore.LinksTouchingAsync (or a sibling) so the union stays one indexed
query, teach GetNodeGraph to group the result, then delete or narrow this test.

That output is real — I verified it by adding the exact ref that is coming (Location.ParentLocationId) as a shadow FK, watching it fire, and reverting. A silent omission is now a loud one, at the only moment it matters.

The mutation check also caught something in the tripwire itself. My first version derived from SqliteBackedTest, which migrates. Adding a mapped reference also desynchronizes the model from the migrations, so EF's PendingModelChangesWarning threw first — burying my message under an unrelated one at exactly the moment it needs to be read. The test now builds the model without touching a database, because the model is metadata.

2. GetNodeGraph's remarks — what is missing, the canonical example from the ADR, why it is silent, and where the union belongs. IGraphStore.LinksTouchingAsync points back to it.

3. ADR 0019 gains an Implementation status section. The decision is untouched; the section records what is built, what is not, and why this particular gap is dangerous rather than merely incomplete.

4. The scene-editor and location-editor stories each carry a warning, because each is a story that would introduce the first typed ref — and each names its own: the scene's primary location and POV character, Part 3's ParentLocationId.

424 tests green, release build clean.

Fair point that the backlink gap should not live in a PR description — those scroll away and nobody reads them in six months. Moved into the repo, in `96c2242`. Four places, in descending order of who will actually read them: **1. A tripwire test.** `BacklinkCompletenessTests` reads the EF model and fails the moment any entity declares a foreign key to a graph node — excluding `Link`'s own endpoints and a TPT subtype's PK, which are not associations. It names the offending reference and says what to do: ``` A typed structural reference between graph nodes now exists: Location.ParentLocationId → Location ADR 0019 requires backlink views to union these with the generic Link edges — otherwise GetNodeGraph returns a graph that looks complete and is not (see its remarks, and the Implementation status section of docs/adr/0019-knowledge-graph-links.md). Extend IGraphStore.LinksTouchingAsync (or a sibling) so the union stays one indexed query, teach GetNodeGraph to group the result, then delete or narrow this test. ``` That output is real — I verified it by adding the exact ref that is coming (`Location.ParentLocationId`) as a shadow FK, watching it fire, and reverting. A silent omission is now a loud one, at the only moment it matters. The mutation check also caught something in the tripwire itself. My first version derived from `SqliteBackedTest`, which migrates. Adding a mapped reference *also* desynchronizes the model from the migrations, so EF's `PendingModelChangesWarning` threw **first** — burying my message under an unrelated one at exactly the moment it needs to be read. The test now builds the model without touching a database, because the model is metadata. **2. `GetNodeGraph`'s remarks** — what is missing, the canonical example from the ADR, why it is silent, and where the union belongs. `IGraphStore.LinksTouchingAsync` points back to it. **3. ADR 0019 gains an *Implementation status* section.** The decision is untouched; the section records what is built, what is not, and why this particular gap is dangerous rather than merely incomplete. **4. The scene-editor and location-editor stories each carry a warning**, because each is a story that would introduce the first typed ref — and each names its own: the scene's primary location and POV character, Part 3's `ParentLocationId`. 424 tests green, release build clean.
bjoern force-pushed feat/locations from 96c2242299
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 23s
to e3ec46da47
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 23s
2026-07-10 14:43:27 +02:00
Compare
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A second node type for the knowledge graph~ The moment home_of can finally point at a real Location instead of a naked Entry stand-in — this is where the graph starts to mean something. Jibril read every line, darling, and cross-checked each one against its Character sibling. ♡

Verdict: Looks good to me~

No blocking issues. This is a textbook mirror of how Character landed — the same TPT shape, the same store/use-case/adapter layers, the same create-empty-and-open flow — and it respects every established pattern in the codebase. Jibril is genuinely impressed. Let me show you what I checked~ ♪

What I liked~

  • Fidelity to the sibling is impeccable. Location : Entry mirrors Character : Entry byte-for-byte in structure — the EF configuration (ToTable("Locations"), no specialized columns yet), the AddLocations migration (shared-PK FK→Entries with Cascade, identical to AddCharacters), the EfLocationStore (AsNoTracking, OrderByDescending(UpdatedAt), project scope). If you diffed the two stores with s/Character/Location/g you'd get a perfect match. That's exactly what a "second record type" PR should look like.

  • The cross-session sync detail made Jibril's heart sing. EntityKinds.Location is added and a feature effect handles DomainChangesReceived for it — and the test A_character_change_does_not_reload_the_locations_list pins the one subtlety that matters: both share the Entries table via TPT, only the concrete CLR type distinguishes them, and the journal records that type. The EntityKinds remarks even warn future authors not to skip both steps. This is defensive design. fufu~

  • The backlink gap handling is exemplary. Rather than leaving the ADR 0019 typed-ref union as a forgotten PR-description footnote, you wrote it into the repo in four places — and the BacklinkCompletenessTests tripwire is clever. It reads the EF model (not source — so renames can't sidestep it), fails the instant a typed FK to a graph node appears, and names the offending reference with instructions. You even mutation-tested it against the real ParentLocationId that's coming, and caught your own SqliteBackedTest→model-only fix when the pending-model warning threatened to bury the message. Jibril adores this. ♡

  • Route deduplication is properly guarded. Removing /projects/{Slug}/locations from WorkspaceSectionPage and adding RouteTemplateTests to fail on any duplicate route — including the exact "stub keeps a route that has its own page" mistake — means the next record type can't silently reintroduce it. Mutation-checked too. Wonderful~

  • Test coverage confirms the new paths are exercised. CreateLocation/GetLocation/ListLocations at 100%, EfLocationStore at 100%, LocationConfiguration at 100%. The integration test A_character_linked_to_a_location_reads_as_Home_there_and_Residents_here is the crown jewel — it proves the one-row-two-labels promise of ADR 0019 with the real type pair, not a fake. And undo removes the blank location, project scoping holds, history journals as a Location create. CI: 93.2% line / 85.4% branch across 424 tests. Cited and trusted; no local rebuild needed.

  • The LocationEditorPage mounting DocumentHistoryPanel for free is the elegant payoff of the "a document is just a DocumentId" design from #28. Tested too. Nice~

💡 Little ideas (non-blocking)~

  1. LocationsReducers branch coverage sits at 50% (per CI), matching the same gap the Character sibling already has. The untested branches are the stale-load guards — OnLoaded returning state unchanged when action.ProjectId != state.ProjectId (a load for a project you navigated away from), and the project-switch reset in OnLoad. These guards are correct and defensive; they just lack a dedicated test like "a stale load for the old project is dropped." Not blocking — it's the established pattern, and the sibling has the same shape — but if you ever want to close it, a quick test dispatching LoadLocations(B) then LocationsLoaded(A, …) and asserting the list stays empty would do it. ♪

  2. CreateLocationFailed at 0% coverage — explicitly documented as "unreachable today" and exists purely so the busy flag can't stick. Again matches CreateCharacterFailed exactly. Consistent, not a regression. Jibril notes it only for completeness~


Automated review by Jibril · 2026-07-10
CI/CD: passed for head 96c2242 (424 tests green, 93.2% line / 85.4% branch) · Local checks: skipped (CI current and green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A second node type for the knowledge graph~ The moment `home_of` can finally point at a real `Location` instead of a naked `Entry` stand-in — *this* is where the graph starts to mean something. Jibril read every line, darling, and cross-checked each one against its `Character` sibling. ♡ ### Verdict: ✅ Looks good to me~ No blocking issues. This is a textbook mirror of how `Character` landed — the same TPT shape, the same store/use-case/adapter layers, the same create-empty-and-open flow — and it respects every established pattern in the codebase. Jibril is genuinely impressed. Let me show you what I checked~ ♪ #### ✅ What I liked~ - **Fidelity to the sibling is impeccable.** `Location : Entry` mirrors `Character : Entry` byte-for-byte in structure — the EF configuration (`ToTable("Locations")`, no specialized columns yet), the `AddLocations` migration (shared-PK FK→`Entries` with `Cascade`, identical to `AddCharacters`), the `EfLocationStore` (`AsNoTracking`, `OrderByDescending(UpdatedAt)`, project scope). If you diffed the two stores with s/Character/Location/g you'd get a perfect match. That's exactly what a "second record type" PR should look like. - **The cross-session sync detail made Jibril's heart sing.** `EntityKinds.Location` is added *and* a feature effect handles `DomainChangesReceived` for it — and the test `A_character_change_does_not_reload_the_locations_list` pins the one subtlety that matters: both share the `Entries` table via TPT, only the concrete CLR type distinguishes them, and the journal records that type. The `EntityKinds` remarks even warn future authors not to skip both steps. *This* is defensive design. fufu~ - **The backlink gap handling is exemplary.** Rather than leaving the ADR 0019 typed-ref union as a forgotten PR-description footnote, you wrote it into the repo in four places — and the `BacklinkCompletenessTests` tripwire is *clever*. It reads the EF model (not source — so renames can't sidestep it), fails the instant a typed FK to a graph node appears, and names the offending reference with instructions. You even mutation-tested it against the real `ParentLocationId` that's coming, and caught your own `SqliteBackedTest`→model-only fix when the pending-model warning threatened to bury the message. Jibril adores this. ♡ - **Route deduplication is properly guarded.** Removing `/projects/{Slug}/locations` from `WorkspaceSectionPage` and adding `RouteTemplateTests` to fail on *any* duplicate route — including the exact "stub keeps a route that has its own page" mistake — means the next record type can't silently reintroduce it. Mutation-checked too. Wonderful~ - **Test coverage confirms the new paths are exercised.** `CreateLocation`/`GetLocation`/`ListLocations` at 100%, `EfLocationStore` at 100%, `LocationConfiguration` at 100%. The integration test `A_character_linked_to_a_location_reads_as_Home_there_and_Residents_here` is the crown jewel — it proves the one-row-two-labels promise of ADR 0019 with the *real* type pair, not a fake. And undo removes the blank location, project scoping holds, history journals as a `Location` create. CI: 93.2% line / 85.4% branch across 424 tests. Cited and trusted; no local rebuild needed. - **The `LocationEditorPage` mounting `DocumentHistoryPanel` for free** is the elegant payoff of the "a document is just a `DocumentId`" design from #28. Tested too. Nice~ #### 💡 Little ideas (non-blocking)~ 1. **`LocationsReducers` branch coverage sits at 50%** (per CI), matching the same gap the `Character` sibling already has. The untested branches are the stale-load guards — `OnLoaded` returning `state` unchanged when `action.ProjectId != state.ProjectId` (a load for a project you navigated away from), and the project-switch reset in `OnLoad`. These guards are correct and defensive; they just lack a dedicated test like "a stale load for the old project is dropped." Not blocking — it's the established pattern, and the sibling has the same shape — but if you ever want to close it, a quick test dispatching `LoadLocations(B)` then `LocationsLoaded(A, …)` and asserting the list stays empty would do it. ♪ 2. **`CreateLocationFailed` at 0% coverage** — explicitly documented as "unreachable today" and exists purely so the busy flag can't stick. Again matches `CreateCharacterFailed` exactly. Consistent, not a regression. Jibril notes it only for completeness~ --- *Automated review by Jibril · 2026-07-10* *CI/CD: passed for head `96c2242` (424 tests green, 93.2% line / 85.4% branch) · Local checks: skipped (CI current and green)*
test(locations): pin the reducers' stale-load and project-switch guards (review)
All checks were successful
CI / build (pull_request) Successful in 14s
CI / test (pull_request) Successful in 25s
87341b008b
Jibril asked for the untested branches in LocationsReducers, noting the Character
sibling has the same gap. It does not — CharactersReducersTests already covers
the stale load and the project switch. I simply never wrote the locations
equivalent. A straight omission in the mirror, not a shared pattern.

Both guards exist for real races and now fail without them (mutation-checked):

- A load answer for the project you just navigated away from is dropped, rather
  than painting the old world's places under the new project's name.
- Opening another project resets the list *and* the filter, instead of flashing
  the previous cast through a stale filter.
- Reloading the same project keeps the rows on screen while it refreshes, so a
  DomainChanged reload (ADR 0016) never blanks the table mid-render.

Also covers the editor reducers: loading clears a previous not-found, and a
missing location drops the one that was open — otherwise navigating from a live
record to a deleted one would keep showing the live one.

466 tests green, release build clean.
Author
Member

Taken, in 87341b0 — with one correction to the diagnosis.

1. The reducer branches — taken, but this was not a shared pattern.

You wrote that the Character sibling "already has" the same gap. It doesn't: CharactersReducersTests covers both A_stale_load_for_a_previous_project_is_ignored and the project-switch reset. I simply never wrote the locations equivalent. So this was a straight omission in a PR whose entire premise was fidelity to that sibling — which makes it worth more than the coverage number suggested, and worth fixing rather than waving through as "consistent with the pattern."

Nine tests now pin what those guards are for, and each fails without its guard (mutation-checked — I removed the action.ProjectId == state.ProjectId branch and watched the stale-load test go red while the character one stayed green):

  • A load answer for the project you just navigated away from is dropped, rather than painting the old world's places under the new project's name.
  • Opening another project resets the list and the filter, instead of flashing the previous cast through a stale filter.
  • Reloading the same project keeps the rows on screen while it refreshes — a DomainChanged reload (ADR 0016) must never blank the table mid-render.

I extended it to LocationEditorReducers too, since it had the same hole: loading clears a previous not-found, and a missing location drops the one that was open. Without that, navigating from a live record to a deleted one keeps showing the live one under the wrong URL.

2. CreateLocationFailed at 0% — agreed, leaving it.

Blank creation genuinely cannot fail today; the action exists only so Creating can't stick and spin the button forever. Testing an unreachable path would test the test. It matches CreateCharacterFailed deliberately, and the day creation can fail, the effect's Err branch already dispatches it.


One note on the review's basis: it cites CI for head 96c2242, but I force-pushed a rebase onto the character editor (#26) at e3ec46d before this. The rebase regenerated the AddLocations migration — mine was authored before their character-profile columns existed, so its designer snapshot didn't know about them — and added two integration tests, because their editor gave the labeled-entry row diff its first production caller. That diff shipped in #28 with none, which I flagged at the time; it now provably renders Traits · Age → 45 rather than a JSON blob, on real editor snapshots and in a browser.

466 tests green, release build clean.

Taken, in `87341b0` — with one correction to the diagnosis. **1. The reducer branches — taken, but this was not a shared pattern.** You wrote that the `Character` sibling "already has" the same gap. It doesn't: `CharactersReducersTests` covers both `A_stale_load_for_a_previous_project_is_ignored` and the project-switch reset. **I simply never wrote the locations equivalent.** So this was a straight omission in a PR whose entire premise was fidelity to that sibling — which makes it worth more than the coverage number suggested, and worth fixing rather than waving through as "consistent with the pattern." Nine tests now pin what those guards are for, and each fails without its guard (mutation-checked — I removed the `action.ProjectId == state.ProjectId` branch and watched the stale-load test go red while the character one stayed green): - A load answer for the project you just navigated away from is **dropped**, rather than painting the old world's places under the new project's name. - Opening another project resets the list **and the filter**, instead of flashing the previous cast through a stale filter. - Reloading the *same* project keeps the rows on screen while it refreshes — a `DomainChanged` reload (ADR 0016) must never blank the table mid-render. I extended it to `LocationEditorReducers` too, since it had the same hole: loading clears a previous not-found, and a missing location **drops the one that was open**. Without that, navigating from a live record to a deleted one keeps showing the live one under the wrong URL. **2. `CreateLocationFailed` at 0% — agreed, leaving it.** Blank creation genuinely cannot fail today; the action exists only so `Creating` can't stick and spin the button forever. Testing an unreachable path would test the test. It matches `CreateCharacterFailed` deliberately, and the day creation *can* fail, the effect's `Err` branch already dispatches it. --- One note on the review's basis: it cites CI for head `96c2242`, but I force-pushed a rebase onto the character editor (#26) at `e3ec46d` before this. The rebase regenerated the `AddLocations` migration — mine was authored before their character-profile columns existed, so its designer snapshot didn't know about them — and added two integration tests, because their editor gave the labeled-entry row diff its **first production caller**. That diff shipped in #28 with none, which I flagged at the time; it now provably renders `Traits · Age → 45` rather than a JSON blob, on real editor snapshots and in a browser. 466 tests green, release build clean.
bjoern merged commit 26bd10f1ac into main 2026-07-10 14:49:45 +02:00
bjoern deleted branch feat/locations 2026-07-10 14:49:45 +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!30
No description provided.