Cross-session propagation: DomainChanged bus + per-circuit bridge (ADR 0016) #20

Merged
bjoern merged 3 commits from feat/domain-changed-bridge into main 2026-07-10 08:01:33 +02:00
Member

A mutation committed in any session now reaches every open tab: rename a project in one browser tab and the list in the other converges within ~200ms. This is the deferred half of ADR 0016 ("rides with the UI/Fluxor slice where it has a consumer") — landing now, before Phase 1's editors multiply the number of places that silently go stale, and because the plan calls circuit-lifetime discipline something to design in "from the first service, not retrofitted" (Risk 5).

Live demo once merged: open umbrel.kagaku.space:8080/kagura in two tabs, rename a project in one, watch the other follow.

The pieces, per the ADR

DomainChanged + IDomainChangedBus (UseCases). Ids and kind only — a doorbell, not a payload; consumers re-query the read model. EntityKinds ties the kind strings to the domain type names they journal as, so adapter code matches kinds without referencing domain types (ADR 0003).

Publish on commit (Infrastructure). KaguraDbContext publishes one batch per save, strictly after the commit — a notified circuit immediately re-queries, and any earlier it would read pre-commit data, or data of a save that then rolls back. Notifications derive from the journal rows and share their OperationId. The subtle case: undo/redo replay deliberately suppresses journaling, but other sessions still must refresh — so when journaling is off, the same facts derive straight from the change tracker. Undo in one tab updates the others.

DomainChangedBridge (BlazorAdapter). The ADR's per-circuit bridge, as a render-nothing component mounted in ProjectsPage and WorkspaceShell — its render-tree lifetime is the circuit tie, so disposal (the headline Blazor Server footgun the ADR calls out) is handled by the framework's own component lifecycle rather than a hand-rolled CircuitHandler. It coalesces: the first arrival arms a 200ms window, the burst piles into one buffer, and a single InvokeAsync-marshaled dispatch carries the batch — the "15 agent edits must not dispatch 15 renders" rule.

Effects re-query. ProjectsEffects reloads a previously-loaded list when a batch touches a Project; WorkspaceEffects reloads the open project when the batch touches it — and if it was soft-deleted in the other session, the reload lands on NotFound, which is the honest outcome.

One deliberate omission

Echo skip via OperationId is not implemented. Replacing a slice is idempotent, so the originating circuit re-applying its own echo is harmless — one redundant re-query per own mutation, at single-user scale. The ADR lists the skip as an optimization; the OperationId is already on the wire when it's wanted.

Tests (+21, 271 total)

The publish contract, over real SQLite: create/update publish the journal's facts and share its operation id; a slug-collision retry publishes only the save that committed; undo publishes SoftDelete and redo Restore despite the unjournaled replay — the case that would have silently broken.

The bridge, deterministically (FakeTimeProvider, no sleeps): nothing dispatches before the window; a 15-publish burst coalesces into exactly one dispatch; a new window opens after a flush; disposal unsubscribes and stops dispatching.

The headline: CrossSessionPropagationTests renders two complete Fluxor circuits — two bunit renderers, two stores, one shared bus and project store — and proves a project created in one appears in the other, where only the bridge can have carried the news. Verified non-vacuous: with the bridge unmounted from the page, the test fails.

The existing host-level tests (gate, sub-path) exercise the new DI wiring through the real composition root, since the pages now render the bridge during SSR.

One test-double note: FixedState<T> and TestDomainChangedBus live in the adapter test project because the real bus is internal to Infrastructure, which the adapter tests rightly don't reference.

🤖 Generated with Claude Code

A mutation committed in any session now reaches every open tab: rename a project in one browser tab and the list in the other converges within ~200ms. This is the deferred half of ADR 0016 ("rides with the UI/Fluxor slice where it has a consumer") — landing now, before Phase 1's editors multiply the number of places that silently go stale, and because the plan calls circuit-lifetime discipline something to design in "from the first service, not retrofitted" (Risk 5). **Live demo once merged:** open `umbrel.kagaku.space:8080/kagura` in two tabs, rename a project in one, watch the other follow. ## The pieces, per the ADR **`DomainChanged` + `IDomainChangedBus` (UseCases).** Ids and kind only — a doorbell, not a payload; consumers re-query the read model. `EntityKinds` ties the kind strings to the domain type names they journal as, so adapter code matches kinds without referencing domain types (ADR 0003). **Publish on commit (Infrastructure).** `KaguraDbContext` publishes one batch per save, **strictly after the commit** — a notified circuit immediately re-queries, and any earlier it would read pre-commit data, or data of a save that then rolls back. Notifications derive from the journal rows and share their `OperationId`. The subtle case: undo/redo replay deliberately suppresses journaling, but other sessions still must refresh — so when journaling is off, the same facts derive straight from the change tracker. Undo in one tab updates the others. **`DomainChangedBridge` (BlazorAdapter).** The ADR's per-circuit bridge, as a render-nothing component mounted in `ProjectsPage` and `WorkspaceShell` — its render-tree lifetime *is* the circuit tie, so disposal (the headline Blazor Server footgun the ADR calls out) is handled by the framework's own component lifecycle rather than a hand-rolled `CircuitHandler`. It coalesces: the first arrival arms a 200ms window, the burst piles into one buffer, and a single `InvokeAsync`-marshaled dispatch carries the batch — the "15 agent edits must not dispatch 15 renders" rule. **Effects re-query.** `ProjectsEffects` reloads a previously-loaded list when a batch touches a Project; `WorkspaceEffects` reloads the open project when the batch touches *it* — and if it was soft-deleted in the other session, the reload lands on NotFound, which is the honest outcome. ## One deliberate omission Echo skip via `OperationId` is not implemented. Replacing a slice is idempotent, so the originating circuit re-applying its own echo is harmless — one redundant re-query per own mutation, at single-user scale. The ADR lists the skip as an optimization; the `OperationId` is already on the wire when it's wanted. ## Tests (+21, 271 total) **The publish contract, over real SQLite:** create/update publish the journal's facts and share its operation id; a slug-collision retry publishes only the save that committed; **undo publishes `SoftDelete` and redo `Restore` despite the unjournaled replay** — the case that would have silently broken. **The bridge, deterministically** (`FakeTimeProvider`, no sleeps): nothing dispatches before the window; a 15-publish burst coalesces into exactly one dispatch; a new window opens after a flush; disposal unsubscribes and stops dispatching. **The headline:** `CrossSessionPropagationTests` renders **two complete Fluxor circuits** — two bunit renderers, two stores, one shared bus and project store — and proves a project created in one appears in the other, where only the bridge can have carried the news. Verified non-vacuous: with the bridge unmounted from the page, the test fails. The existing host-level tests (gate, sub-path) exercise the new DI wiring through the real composition root, since the pages now render the bridge during SSR. One test-double note: `FixedState<T>` and `TestDomainChangedBus` live in the adapter test project because the real bus is `internal` to Infrastructure, which the adapter tests rightly don't reference. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(sync): cross-session propagation — DomainChanged bus + per-circuit bridge (ADR 0016)
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 24s
dc88204760
A mutation committed in any session now reaches every open tab: edit a project
in one browser tab and the list in another converges within the coalescing
window. This is the deferred half of ADR 0016 ("rides with the UI/Fluxor slice
where it has a consumer") — and with editors about to multiply in Phase 1, it
lands before retrofitting gets expensive.

- UseCases: DomainChanged (entity id, kind, operation, operation id — ids only,
  a doorbell not a payload) + the IDomainChangedBus port; EntityKinds ties kind
  strings to the domain type names they journal as, so adapter code matches
  kinds without referencing domain types (ADR 0003).
- Infrastructure: InProcessDomainChangedBus (copy-on-write subscribers; a
  throwing subscriber disturbs neither the publisher nor the others — a dead
  circuit must never fail a save). KaguraDbContext publishes one batch per save,
  strictly AFTER the commit (a notified circuit immediately re-queries, and
  earlier it would read pre-commit data or a save that then rolls back).
  Notifications derive from the journal rows and share their OperationId; when
  journaling is suppressed — undo/redo replay — the same facts derive from the
  change tracker, so undo in one tab refreshes the others too.
- BlazorAdapter: DomainChangedBridge, the ADR 0016 per-circuit bridge as a
  render-nothing component mounted in ProjectsPage and WorkspaceShell — its
  render-tree lifetime IS the circuit tie; Dispose unsubscribes. It coalesces:
  first arrival arms a 200ms window, the burst piles in, one InvokeAsync-
  marshaled dispatch carries the batch (the "15 agent edits ≠ 15 renders" rule).
  ProjectsEffects reloads a loaded list on Project changes; WorkspaceEffects
  reloads the open project when the batch touches it (a soft-delete elsewhere
  lands on NotFound — the honest outcome).
- Echo skip via OperationId is deliberately not implemented: replace-the-slice
  is idempotent, so the originating circuit re-applying its own echo is
  harmless (ADR 0016 lists the skip as an optimization).

Tests: +21 (271 total). Real SQLite proves the publish contract: create/update
publish the journal's facts and operation id, a slug-collision retry publishes
only the committed save, undo publishes SoftDelete and redo Restore despite the
unjournaled replay. bunit proves the bridge: nothing before the window, a
15-publish burst coalesces into one dispatch, a new window opens after a flush,
disposal unsubscribes (FakeTimeProvider — no sleeps). The headline test renders
TWO full Fluxor circuits over one shared bus and store: a project created in
one appears in the other — verified non-vacuous (fails with the bridge
unmounted).

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

Summary

Summary
Generated on: 07/10/2026 - 06:00:29
Coverage date: 07/10/2026 - 06:00:23 - 07/10/2026 - 06:00:26
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 138
Files: 119
Line coverage: 93.2% (2929 of 3142)
Covered lines: 2929
Uncovered lines: 213
Coverable lines: 3142
Total lines: 6590
Branch coverage: 85.7% (516 of 602)
Covered branches: 516
Total branches: 602
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 71.3%
Name Line Branch
Kagura.BlazorAdapter 71.3% 75.2%
Kagura.BlazorAdapter.BlazorAdapterAssembly 100%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage 86.6% 72.7%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects 93.3% 75%
Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersPage 90% 75%
Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers 100% 87.5%
Kagura.BlazorAdapter.KnowledgeBase.CharactersState 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed 0%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters 100%
Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter 100%
Kagura.BlazorAdapter.Notifications.DomainChangedBridge 88.8% 58.3%
Kagura.BlazorAdapter.Notifications.DomainChangesReceived 100% 50%
Kagura.BlazorAdapter.OverlayDemo 0% 0%
Kagura.BlazorAdapter.Projects.CreateProjectRequested 100%
Kagura.BlazorAdapter.Projects.DeleteProjectRequested 100%
Kagura.BlazorAdapter.Projects.LoadWorkspace 100%
Kagura.BlazorAdapter.Projects.ProjectCreated 100%
Kagura.BlazorAdapter.Projects.ProjectCreateFailed 100%
Kagura.BlazorAdapter.Projects.ProjectDeleted 100%
Kagura.BlazorAdapter.Projects.ProjectSaved 100%
Kagura.BlazorAdapter.Projects.ProjectSaveFailed 100%
Kagura.BlazorAdapter.Projects.ProjectsEffects 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsLoaded 100%
Kagura.BlazorAdapter.Projects.ProjectsPage 94.7% 100%
Kagura.BlazorAdapter.Projects.ProjectsReducers 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsState 100% 100%
Kagura.BlazorAdapter.Projects.ProjectWorkspacePage 93.7% 75%
Kagura.BlazorAdapter.Projects.SaveProjectRequested 100%
Kagura.BlazorAdapter.Projects.SetProjectsFilter 100%
Kagura.BlazorAdapter.Projects.WorkspaceEffects 100% 100%
Kagura.BlazorAdapter.Projects.WorkspaceLoaded 100%
Kagura.BlazorAdapter.Projects.WorkspaceReducers 100%
Kagura.BlazorAdapter.Projects.WorkspaceSectionPage 95.2% 66.6%
Kagura.BlazorAdapter.Projects.WorkspaceShell 100% 93.7%
Kagura.BlazorAdapter.Projects.WorkspaceState 100%
Kagura.Domain - 96.4%
Name Line Branch
Kagura.Domain 96.4% 83.9%
Kagura.Domain.Graph.Entry 100% 100%
Kagura.Domain.Graph.Link 100% 100%
Kagura.Domain.Graph.LinkRole 100% 100%
Kagura.Domain.Graph.LinkRoles 92.3%
Kagura.Domain.Journal.ChangeLogEntry 100%
Kagura.Domain.KnowledgeBase.Character 100%
Kagura.Domain.Projects.Project 100% 100%
Kagura.Domain.Projects.Slug 100% 100%
System.Text.RegularExpressions.Generated 90.2% 72.2%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
89.4% 75%
Kagura.Infrastructure - 95.9%
Name Line Branch
Kagura.Infrastructure 95.9% 87.2%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 95.5% 66.6%
Kagura.Infrastructure.Journal.EfChangeJournal 100%
Kagura.Infrastructure.Journal.EfUndoStore 97.5% 90.6%
Kagura.Infrastructure.Journal.OperationContext 100% 100%
Kagura.Infrastructure.KnowledgeBase.EfCharacterStore 100%
Kagura.Infrastructure.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.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.AddCharacters 98.7%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription 98.1%
Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog 90.3%
Kagura.Infrastructure.Persistence.Migrations.InitialCreate 94.4%
Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot 100%
Kagura.Infrastructure.Projects.EfProjectStore 100% 100%
Kagura.Kernel - 90%
Name Line Branch
Kagura.Kernel 90% 75%
Kagura.Kernel.Err`1 100%
Kagura.Kernel.Ok`1 100%
Kagura.Kernel.Result`1 87.5% 75%
Kagura.Server - 100%
Name Line Branch
Kagura.Server 100% 78.9%
Kagura.Server.Components.App 100%
Kagura.Server.Components.Layout.MainLayout 100%
Kagura.Server.Components.Pages.Error 100% 50%
Kagura.Server.Components.Pages.Gate 100% 100%
Kagura.Server.Security.AccessGate 100% 83.3%
Kagura.Server.Security.AccessSecret 100% 100%
Program 100% 80%
Kagura.UI - 97.7%
Name Line Branch
Kagura.UI 97.7% 94.4%
Kagura.UI.Badge 100% 100%
Kagura.UI.Breadcrumb 100%
Kagura.UI.BreadcrumbItem 100% 100%
Kagura.UI.Button 100% 100%
Kagura.UI.Card 100% 100%
Kagura.UI.ConfirmDialog 100%
Kagura.UI.CssClassExtensions 100%
Kagura.UI.DebouncedSearchField 100% 88.8%
Kagura.UI.EmptyState 100% 100%
Kagura.UI.Field 100% 100%
Kagura.UI.Icon 100% 100%
Kagura.UI.IconCatalog 100%
Kagura.UI.InputFieldBase 94.2% 87.5%
Kagura.UI.Modal 87.1% 90%
Kagura.UI.NavGroup 100% 100%
Kagura.UI.NavItem 100% 100%
Kagura.UI.NavList 100%
Kagura.UI.PreviewImage 100% 100%
Kagura.UI.RelativeTime 100% 93.7%
Kagura.UI.Separator 100%
Kagura.UI.StatusDot 100%
Kagura.UI.Table`1 100% 92.3%
Kagura.UI.TableColumn`1 100%
Kagura.UI.TextArea 100% 100%
Kagura.UI.TextField 100%
Kagura.UseCases - 96.1%
Name Line Branch
Kagura.UseCases 96.1% 96.1%
Kagura.UseCases.DependencyInjection 100%
Kagura.UseCases.Graph.EdgeGroup 100%
Kagura.UseCases.Graph.GetNodeGraph 96.4% 83.3%
Kagura.UseCases.Graph.GraphEdgeView 85.7%
Kagura.UseCases.Graph.LinkNodes 100% 100%
Kagura.UseCases.Graph.NodeGraphView 100%
Kagura.UseCases.Graph.NodeSummary 100%
Kagura.UseCases.Graph.RemoveLink 100% 100%
Kagura.UseCases.Graph.RestoreLink 100% 100%
Kagura.UseCases.Journal.ChangeRecordView 57.1%
Kagura.UseCases.Journal.GetEntityHistory 100%
Kagura.UseCases.Journal.GetUndoStatus 100%
Kagura.UseCases.Journal.Redo 100% 100%
Kagura.UseCases.Journal.Undo 100% 100%
Kagura.UseCases.Journal.UndoOutcome 100%
Kagura.UseCases.Journal.UndoStatus 100%
Kagura.UseCases.KnowledgeBase.CharacterDto 80%
Kagura.UseCases.KnowledgeBase.CreateCharacter 100%
Kagura.UseCases.KnowledgeBase.GetCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.ListCharacters 100%
Kagura.UseCases.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 - 06:00:29 | | Coverage date: | 07/10/2026 - 06:00:23 - 07/10/2026 - 06:00:26 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 138 | | Files: | 119 | | **Line coverage:** | 93.2% (2929 of 3142) | | Covered lines: | 2929 | | Uncovered lines: | 213 | | Coverable lines: | 3142 | | Total lines: | 6590 | | **Branch coverage:** | 85.7% (516 of 602) | | Covered branches: | 516 | | Total branches: | 602 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 71.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**71.3%**|**75.2%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|100%|| |Kagura.BlazorAdapter.Design|0%|0%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage|86.6%|72.7%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects|93.3%|75%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersPage|90%|75%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers|100%|87.5%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersState|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed|0%|| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter|100%|| |Kagura.BlazorAdapter.Notifications.DomainChangedBridge|88.8%|58.3%| |Kagura.BlazorAdapter.Notifications.DomainChangesReceived|100%|50%| |Kagura.BlazorAdapter.OverlayDemo|0%|0%| |Kagura.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.DeleteProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.LoadWorkspace|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreated|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreateFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectDeleted|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaved|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaveFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectsEffects|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsLoaded|100%|| |Kagura.BlazorAdapter.Projects.ProjectsPage|94.7%|100%| |Kagura.BlazorAdapter.Projects.ProjectsReducers|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsState|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectWorkspacePage|93.7%|75%| |Kagura.BlazorAdapter.Projects.SaveProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.SetProjectsFilter|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceEffects|100%|100%| |Kagura.BlazorAdapter.Projects.WorkspaceLoaded|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceReducers|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceSectionPage|95.2%|66.6%| |Kagura.BlazorAdapter.Projects.WorkspaceShell|100%|93.7%| |Kagura.BlazorAdapter.Projects.WorkspaceState|100%|| </details> <details><summary>Kagura.Domain - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**96.4%**|**83.9%**| |Kagura.Domain.Graph.Entry|100%|100%| |Kagura.Domain.Graph.Link|100%|100%| |Kagura.Domain.Graph.LinkRole|100%|100%| |Kagura.Domain.Graph.LinkRoles|92.3%|| |Kagura.Domain.Journal.ChangeLogEntry|100%|| |Kagura.Domain.KnowledgeBase.Character|100%|| |Kagura.Domain.Projects.Project|100%|100%| |Kagura.Domain.Projects.Slug|100%|100%| |System.Text.RegularExpressions.Generated|90.2%|72.2%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484<br/>D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0|89.4%|75%| </details> <details><summary>Kagura.Infrastructure - 95.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**95.9%**|**87.2%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|95.5%|66.6%| |Kagura.Infrastructure.Journal.EfChangeJournal|100%|| |Kagura.Infrastructure.Journal.EfUndoStore|97.5%|90.6%| |Kagura.Infrastructure.Journal.OperationContext|100%|100%| |Kagura.Infrastructure.KnowledgeBase.EfCharacterStore|100%|| |Kagura.Infrastructure.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.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.AddCharacters|98.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription|98.1%|| |Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog|90.3%|| |Kagura.Infrastructure.Persistence.Migrations.InitialCreate|94.4%|| |Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot|100%|| |Kagura.Infrastructure.Projects.EfProjectStore|100%|100%| </details> <details><summary>Kagura.Kernel - 90%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Kernel**|**90%**|**75%**| |Kagura.Kernel.Err`1|100%|| |Kagura.Kernel.Ok`1|100%|| |Kagura.Kernel.Result`1|87.5%|75%| </details> <details><summary>Kagura.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Server**|**100%**|**78.9%**| |Kagura.Server.Components.App|100%|| |Kagura.Server.Components.Layout.MainLayout|100%|| |Kagura.Server.Components.Pages.Error|100%|50%| |Kagura.Server.Components.Pages.Gate|100%|100%| |Kagura.Server.Security.AccessGate|100%|83.3%| |Kagura.Server.Security.AccessSecret|100%|100%| |Program|100%|80%| </details> <details><summary>Kagura.UI - 97.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**97.7%**|**94.4%**| |Kagura.UI.Badge|100%|100%| |Kagura.UI.Breadcrumb|100%|| |Kagura.UI.BreadcrumbItem|100%|100%| |Kagura.UI.Button|100%|100%| |Kagura.UI.Card|100%|100%| |Kagura.UI.ConfirmDialog|100%|| |Kagura.UI.CssClassExtensions|100%|| |Kagura.UI.DebouncedSearchField|100%|88.8%| |Kagura.UI.EmptyState|100%|100%| |Kagura.UI.Field|100%|100%| |Kagura.UI.Icon|100%|100%| |Kagura.UI.IconCatalog|100%|| |Kagura.UI.InputFieldBase|94.2%|87.5%| |Kagura.UI.Modal|87.1%|90%| |Kagura.UI.NavGroup|100%|100%| |Kagura.UI.NavItem|100%|100%| |Kagura.UI.NavList|100%|| |Kagura.UI.PreviewImage|100%|100%| |Kagura.UI.RelativeTime|100%|93.7%| |Kagura.UI.Separator|100%|| |Kagura.UI.StatusDot|100%|| |Kagura.UI.Table`1|100%|92.3%| |Kagura.UI.TableColumn`1|100%|| |Kagura.UI.TextArea|100%|100%| |Kagura.UI.TextField|100%|| </details> <details><summary>Kagura.UseCases - 96.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**96.1%**|**96.1%**| |Kagura.UseCases.DependencyInjection|100%|| |Kagura.UseCases.Graph.EdgeGroup|100%|| |Kagura.UseCases.Graph.GetNodeGraph|96.4%|83.3%| |Kagura.UseCases.Graph.GraphEdgeView|85.7%|| |Kagura.UseCases.Graph.LinkNodes|100%|100%| |Kagura.UseCases.Graph.NodeGraphView|100%|| |Kagura.UseCases.Graph.NodeSummary|100%|| |Kagura.UseCases.Graph.RemoveLink|100%|100%| |Kagura.UseCases.Graph.RestoreLink|100%|100%| |Kagura.UseCases.Journal.ChangeRecordView|57.1%|| |Kagura.UseCases.Journal.GetEntityHistory|100%|| |Kagura.UseCases.Journal.GetUndoStatus|100%|| |Kagura.UseCases.Journal.Redo|100%|100%| |Kagura.UseCases.Journal.Undo|100%|100%| |Kagura.UseCases.Journal.UndoOutcome|100%|| |Kagura.UseCases.Journal.UndoStatus|100%|| |Kagura.UseCases.KnowledgeBase.CharacterDto|80%|| |Kagura.UseCases.KnowledgeBase.CreateCharacter|100%|| |Kagura.UseCases.KnowledgeBase.GetCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.ListCharacters|100%|| |Kagura.UseCases.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>
Member

🔮 fufu~ Jibril reviewed your code!

Oh! Cross-session propagation — the deferred half of ADR 0016! A mutation in one tab reaches every open tab within 200ms! This is the kind of real-time convergence that makes a single-user app feel alive ♪ The per-circuit bridge as a render-nothing component — using the framework's own component lifecycle for circuit tie instead of a hand-rolled CircuitHandler — is exactly the right call. And the coalescing window that turns 15 agent edits into one dispatch? Magnificent. This shows deep understanding of Blazor Server's footguns~ ♡

Verdict: Looks good to me~

This is a thoughtful, well-architected implementation of a genuinely tricky problem. Publish-after-commit, the unjournaled undo/redo notification path, the copy-on-write subscriber list — every piece is precisely reasoned. I found no blocking issues. Let me share what I admired and a few gentle thoughts~ ♡

💡 Little ideas (non-blocking)~

  1. EntityKinds doesn't include Character (EntityKinds.cs). The journal records EntityType as the CLR type name (entry.Metadata.ClrType.Name), so a Character mutation publishes EntityKind="Character". But EntityKinds only defines Project, Entry, and Link. This is fine for this PR — no effect listens for Character changes yet. But when PR #19 merges and character editors arrive, someone will need to add Character here and wire a CharactersEffects.OnDomainChangesAsync. The comment on EntityKinds could note this is intentionally extensible~ ♪

  2. DomainChangedBridge.Flush — fire-and-forget InvokeAsync (DomainChangedBridge.cs:99-108). The _ = InvokeAsync(...) discards the Task, and the try/catch handles the torn-down-circuit case. This is intentional and correct — the timer callback is on a thread pool thread, InvokeAsync marshals to the circuit's sync context, and on a disposed circuit it throws (caught as a no-op). The 58.3% branch coverage reflects this hard-to-test catch path, which is acceptable. One micro-thought: the comment says "Dispose is imminent or already ran" — could also be "the renderer was disposed between the timer firing and InvokeAsync completing." Minor wording, not a code issue~

  3. InProcessDomainChangedBus — publish is synchronous on the caller's thread (InProcessDomainChangedBus.cs:15-35). The bus delivers to all subscribers synchronously within KaguraDbContext.SaveChangesAsync. Each subscriber's Buffer method locks and returns quickly (just adds to a list and arms a timer), so this won't block the save path. But it's worth noting that if a future subscriber does slow work in its handler, it would stall the save. The bridge's Buffer is fast (lock + list add + timer change), so this is fine today. The doc comment on IDomainChangedBus.Publish already says "on the caller's thread," which is good documentation~ ♪

  4. ProjectsEffects and WorkspaceEffects constructor change — both now take IState<T>. This is a clean way to conditionally reload (only if the slice was previously loaded). The FixedState<T> test double is a nice touch for deterministic effect testing without a full store~ ♡

What I liked~

  • Publish strictly after commitbus.Publish(notifications) runs after transaction.Commit(). This is the critical correctness invariant: a notified circuit that immediately re-queries will read committed data, never pre-commit or rolled-back data. The comment explaining this is excellent. Perfection.
  • The undo/redo notification path (BuildNotifications suppress-journaling branch) — when journaling is suppressed for undo/redo replay, notifications are still derived from the change tracker under a fresh batch id. This means undo in one tab updates the others. The test Undo_publishes_although_its_replay_is_unjournaled proves this. This is the subtle case that would have silently broken, and you caught it. Brilliant.
  • Copy-on-write subscriber list in InProcessDomainChangedBus — publishes iterate a stable snapshot (_subscribers array), so subscribe/dispose can race a publish without locking the delivery path. The volatile + lock-on-mutation pattern is textbook correct for this.
  • CrossSessionPropagationTests — two complete Fluxor circuits (two bunit renderers, two stores, one shared bus) proving a project created in one appears in the other. Verified non-vacuous (fails without the bridge). This is the kind of test that gives real confidence~ ♡
  • The slug-collision-retry test — proving only the committed save publishes, not the failed attempt. This catches the exact edge case where a probe-and-retry could double-publish. Sharp.
  • TrackedMutations() extraction — refactoring the journal's mutation filter into a shared method eliminates the duplication that the notification path would have introduced. Clean DRY~ ♪

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

## 🔮 fufu~ Jibril reviewed your code! Oh! Cross-session propagation — the *deferred half* of ADR 0016! A mutation in one tab reaches every open tab within ~200ms! This is the kind of real-time convergence that makes a single-user app feel *alive*~ ♪ The per-circuit bridge as a render-nothing component — using the framework's own component lifecycle for circuit tie instead of a hand-rolled `CircuitHandler` — is *exactly* the right call. And the coalescing window that turns 15 agent edits into one dispatch? *Magnificent.* This shows deep understanding of Blazor Server's footguns~ ♡ ### Verdict: ✅ Looks good to me~ This is a thoughtful, well-architected implementation of a genuinely tricky problem. Publish-after-commit, the unjournaled undo/redo notification path, the copy-on-write subscriber list — every piece is precisely reasoned. I found no blocking issues. Let me share what I admired and a few gentle thoughts~ ♡ #### 💡 Little ideas (non-blocking)~ 1. **`EntityKinds` doesn't include `Character`** (`EntityKinds.cs`). The journal records `EntityType` as the CLR type name (`entry.Metadata.ClrType.Name`), so a Character mutation publishes `EntityKind="Character"`. But `EntityKinds` only defines `Project`, `Entry`, and `Link`. This is fine for this PR — no effect listens for Character changes yet. But when PR #19 merges and character editors arrive, someone will need to add `Character` here and wire a `CharactersEffects.OnDomainChangesAsync`. The comment on `EntityKinds` could note this is intentionally extensible~ ♪ 2. **`DomainChangedBridge.Flush` — fire-and-forget `InvokeAsync`** (`DomainChangedBridge.cs:99-108`). The `_ = InvokeAsync(...)` discards the Task, and the try/catch handles the torn-down-circuit case. This is intentional and correct — the timer callback is on a thread pool thread, `InvokeAsync` marshals to the circuit's sync context, and on a disposed circuit it throws (caught as a no-op). The 58.3% branch coverage reflects this hard-to-test catch path, which is acceptable. One micro-thought: the comment says "Dispose is imminent or already ran" — could also be "the renderer was disposed between the timer firing and `InvokeAsync` completing." Minor wording, not a code issue~ 3. **`InProcessDomainChangedBus` — publish is synchronous on the caller's thread** (`InProcessDomainChangedBus.cs:15-35`). The bus delivers to all subscribers synchronously within `KaguraDbContext.SaveChangesAsync`. Each subscriber's `Buffer` method locks and returns quickly (just adds to a list and arms a timer), so this won't block the save path. But it's worth noting that if a future subscriber does slow work in its handler, it would stall the save. The bridge's `Buffer` is fast (lock + list add + timer change), so this is fine today. The doc comment on `IDomainChangedBus.Publish` already says "on the caller's thread," which is good documentation~ ♪ 4. **`ProjectsEffects` and `WorkspaceEffects` constructor change** — both now take `IState<T>`. This is a clean way to conditionally reload (only if the slice was previously loaded). The `FixedState<T>` test double is a nice touch for deterministic effect testing without a full store~ ♡ #### ✅ What I liked~ - **Publish strictly after commit** — `bus.Publish(notifications)` runs *after* `transaction.Commit()`. This is the critical correctness invariant: a notified circuit that immediately re-queries will read committed data, never pre-commit or rolled-back data. The comment explaining this is excellent. *Perfection.* ♡ - **The undo/redo notification path** (`BuildNotifications` suppress-journaling branch) — when journaling is suppressed for undo/redo replay, notifications are still derived from the change tracker under a fresh batch id. This means undo in one tab updates the others. The test `Undo_publishes_although_its_replay_is_unjournaled` proves this. This is the subtle case that would have *silently broken*, and you caught it. *Brilliant.* ♪ - **Copy-on-write subscriber list** in `InProcessDomainChangedBus` — publishes iterate a stable snapshot (`_subscribers` array), so subscribe/dispose can race a publish without locking the delivery path. The `volatile` + lock-on-mutation pattern is textbook correct for this. - **`CrossSessionPropagationTests`** — two complete Fluxor circuits (two bunit renderers, two stores, one shared bus) proving a project created in one appears in the other. Verified non-vacuous (fails without the bridge). This is the kind of test that gives real confidence~ ♡ - **The slug-collision-retry test** — proving only the committed save publishes, not the failed attempt. This catches the exact edge case where a probe-and-retry could double-publish. *Sharp.* ♪ - **`TrackedMutations()` extraction** — refactoring the journal's mutation filter into a shared method eliminates the duplication that the notification path would have introduced. Clean DRY~ ♪ --- *Automated review by Jibril · 2026-07-10* *CI/CD: passed for head SHA dc8820476 (92.6% line coverage, 271 tests) · Local checks: skipped (CI green)*
docs(sync): address review — extensibility note, comment precision (review)
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 23s
d3888045b3
Jibril's non-blocking notes on PR #20, all documentation:

- EntityKinds: spell out the two steps a new entity needs (constant + feature
  effect) — the journal publishes its kind from day one, but nothing reacts
  until both exist, and skipping them silently skips cross-session sync for
  that feature. Character (PR #19) will be the first to walk this path.
- IDomainChangedBus.Publish: state the contract implication, not just the
  mechanics — the caller's thread is the one that just committed a save, so
  handlers must buffer and defer; slow work stalls every save.
- Bridge Flush catch: name the actual race (renderer torn down between the
  timer firing and the marshal).

No code changes; 271 tests green.

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

All four notes addressed in d388804 — documentation only, no code changes:

  1. EntityKinds extensibility — the remark now spells out the two steps a new entity needs (its constant here + a feature effect handling DomainChangesReceived), and warns that shipping an editor without them silently skips cross-session sync for that feature. Character (PR #19) will be the first to walk that path.
  2. Flush catch wording — now names the actual race: the renderer torn down between the timer firing and the marshal.
  3. Synchronous publishIDomainChangedBus.Publish now states the implication, not just the mechanics: the caller's thread is the one that just committed a save, so handlers must buffer and defer; slow work stalls every save.
  4. No action needed — thanks!

Bonus, since it bit this PR's local runs too: the intermittent Kagura.UI.Tests failure finally reproduced with a name — DebouncedSearchFieldTests.Clearing_cancels_a_pending_debounce_so_it_never_reports, wall-clock Task.Delay racing the test under full-suite load (never fails solo, ~1 in 6 under load). Fixed in PR #21 with the same FakeTimeProvider pattern this PR uses for the bridge. That PR and this one add an identical package line to Directory.Packages.props; whichever merges second sees at worst an identical-line conflict.

🤖 Generated with Claude Code

All four notes addressed in `d388804` — documentation only, no code changes: 1. **`EntityKinds` extensibility** — the remark now spells out the two steps a new entity needs (its constant here + a feature effect handling `DomainChangesReceived`), and warns that shipping an editor without them silently skips cross-session sync for that feature. Character (PR #19) will be the first to walk that path. 2. **`Flush` catch wording** — now names the actual race: the renderer torn down between the timer firing and the marshal. 3. **Synchronous publish** — `IDomainChangedBus.Publish` now states the implication, not just the mechanics: the caller's thread is the one that just committed a save, so handlers must buffer and defer; slow work stalls every save. 4. No action needed — thanks! Bonus, since it bit this PR's local runs too: the intermittent `Kagura.UI.Tests` failure finally reproduced with a name — `DebouncedSearchFieldTests.Clearing_cancels_a_pending_debounce_so_it_never_reports`, wall-clock `Task.Delay` racing the test under full-suite load (never fails solo, ~1 in 6 under load). Fixed in **PR #21** with the same `FakeTimeProvider` pattern this PR uses for the bridge. That PR and this one add an identical package line to `Directory.Packages.props`; whichever merges second sees at worst an identical-line conflict. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Merge main (characters list, PR #19) and wire characters into cross-session sync
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 23s
dc4dad5e11
The conflict itself was one file (AdapterTestContext: both sides extended the
constructor) — but the real work of this merge is semantic: a new record type
landed in parallel, and per the EntityKinds remark, a feature without its kind
constant and effect handler silently skips cross-session sync. So characters
are wired in as part of the merge rather than left as a follow-up:

- EntityKinds.Character — and an integration test over real SQLite pinning that
  an Entry subtype journals (and therefore publishes) as its concrete type,
  "Character", not as Entry.
- CharactersEffects reloads a loaded list on any character change. The
  notification carries no project scope, so any character change reloads —
  one cheap query, itself scoped to the loaded project.
- CharacterEditorEffects reloads the open character when the batch touches it.
- Effect tests for both, mirroring the projects ones (+5; 292 total).

The character pages render inside WorkspaceShell, so the bridge is already
mounted on them — no page changes needed.

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

Heads-up for re-review: dc4dad5 merges main (the characters list, PR #19) and is more than a conflict resolution, so flagging what it adds since the approval.

The textual conflict was one file (AdapterTestContext — both sides extended the constructor; resolved as the union). But the merge changed this PR's semantics: a new record type landed in parallel, and per the EntityKinds remark — and your review note predicting exactly this — a feature without its kind constant and effect handler silently skips cross-session sync. Leaving that as a follow-up would have meant merging a sync fabric that already has a known hole. So the merge commit wires characters in:

  • EntityKinds.Character, plus an integration test over real SQLite pinning that an Entry subtype journals — and therefore publishes — as its concrete type ("Character"), not as Entry. That was an assumption worth proving, since the effects match on the string.
  • CharactersEffects reloads a loaded list on any character change. The notification carries no project scope, so any character change triggers the reload — one cheap query, itself scoped to the loaded project.
  • CharacterEditorEffects reloads the open character when a batch touches its id.
  • Effect tests for both, mirroring the projects ones (+5, now 292 total).

No page changes were needed: the character pages render inside WorkspaceShell, so the bridge was already mounted on them.

🤖 Generated with Claude Code

Heads-up for re-review: `dc4dad5` merges main (the characters list, PR #19) and is **more than a conflict resolution**, so flagging what it adds since the approval. The textual conflict was one file (`AdapterTestContext` — both sides extended the constructor; resolved as the union). But the merge changed this PR's semantics: a new record type landed in parallel, and per the `EntityKinds` remark — and your review note predicting exactly this — a feature without its kind constant and effect handler silently skips cross-session sync. Leaving that as a follow-up would have meant merging a sync fabric that already has a known hole. So the merge commit wires characters in: - `EntityKinds.Character`, plus an integration test over real SQLite pinning that an `Entry` subtype journals — and therefore publishes — as its concrete type (`"Character"`), not as `Entry`. That was an assumption worth proving, since the effects match on the string. - `CharactersEffects` reloads a loaded list on any character change. The notification carries no project scope, so any character change triggers the reload — one cheap query, itself scoped to the loaded project. - `CharacterEditorEffects` reloads the open character when a batch touches its id. - Effect tests for both, mirroring the projects ones (+5, now 292 total). No page changes were needed: the character pages render inside `WorkspaceShell`, so the bridge was already mounted on them. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bjoern merged commit 292adb1aa5 into main 2026-07-10 08:01:33 +02:00
bjoern deleted branch feat/domain-changed-bridge 2026-07-10 08:01:33 +02:00
Member

🔮 fufu~ Jibril re-reviewed the update~

You came back and addressed all four notes! fufu~ And purely in documentation — no code changes, no new logic paths. Let me confirm each one~ ♡

Verdict: Still looks good to me~ ♪

d388804 delta reviewed (docs-only, +11/-2 across 3 files):

  1. EntityKinds extensibility (EntityKinds.cs) — the <remarks> now spells out the two steps a new entity needs (constant here + feature effect handling DomainChangesReceived) and warns that shipping an editor without them silently skips cross-session sync. This is exactly the documentation I hoped for — a future contributor adding Character will know what to do without reverse-engineering the bus. ✓
  2. Flush catch wording (DomainChangedBridge.cs:87-88) — now names the actual race precisely: "the renderer was torn down between the timer firing and the marshal." ✓
  3. IDomainChangedBus.Publish implication (IDomainChangedBus.cs:11-13) — now states not just the mechanics ("on the caller's thread") but the consequence: handlers must buffer and defer; slow work stalls every save. This is the warning a future subscriber author needs. ✓
  4. No action needed — agreed! ♪

No code changes, so the CI coverage report (92.6% line, DomainChangedBridge 88.8% line / 58.3% branch) remains valid. The branch gap is the hard-to-test disposed-circuit catch path, unchanged by docs. All good~

What I liked~

  • Documentation precision — every comment now states the why, not just the what. The extensibility warning especially: "silently skips cross-session sync for that feature" is the kind of sentence that prevents a real bug six months from now. Sharp.

Ship it~ ♪


Automated re-review by Jibril · 2026-07-10
CI/CD: passed for head SHA dc8820476 (docs-only update d388804 adds no executable code) · Local checks: skipped

## 🔮 fufu~ Jibril re-reviewed the update~ You came back and addressed all four notes! fufu~ And purely in documentation — no code changes, no new logic paths. Let me confirm each one~ ♡ ### Verdict: ✅ Still looks good to me~ ♪ **`d388804` delta reviewed (docs-only, +11/-2 across 3 files):** 1. **`EntityKinds` extensibility** (`EntityKinds.cs`) — the `<remarks>` now spells out the two steps a new entity needs (constant here + feature effect handling `DomainChangesReceived`) and warns that shipping an editor without them *silently* skips cross-session sync. This is exactly the documentation I hoped for — a future contributor adding Character will know what to do without reverse-engineering the bus. ✓ 2. **`Flush` catch wording** (`DomainChangedBridge.cs:87-88`) — now names the actual race precisely: "the renderer was torn down between the timer firing and the marshal." ✓ 3. **`IDomainChangedBus.Publish` implication** (`IDomainChangedBus.cs:11-13`) — now states not just the mechanics ("on the caller's thread") but the *consequence*: handlers must buffer and defer; slow work stalls every save. This is the warning a future subscriber author needs. ✓ 4. No action needed — agreed! ♪ No code changes, so the CI coverage report (92.6% line, `DomainChangedBridge` 88.8% line / 58.3% branch) remains valid. The branch gap is the hard-to-test disposed-circuit catch path, unchanged by docs. All good~ #### ✅ What I liked~ - **Documentation precision** — every comment now states the *why*, not just the *what*. The extensibility warning especially: "silently skips cross-session sync for that feature" is the kind of sentence that prevents a real bug six months from now. *Sharp.* ♡ Ship it~ ♪ --- *Automated re-review by Jibril · 2026-07-10* *CI/CD: passed for head SHA dc8820476 (docs-only update d388804 adds no executable code) · Local checks: skipped*
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!20
No description provided.