Document history: revisions, diff, and targeted revert (ADR 0022) #28

Merged
bjoern merged 2 commits from feat/document-history into main 2026-07-10 13:51:45 +02:00
Member

Wikipedia-style per-document history: browse a record's past versions, see what changed, and roll that one document back without undoing everything else.

No second store. The ADR 0020 change journal already holds a before/after snapshot for every mutation, so revisions, diffs, and revert are all pure functions of what is persisted. This is the document-scoped lens the ADR promised, not a new source of truth.

The engine

GetDocumentHistory coalesces the journal's fine-grained rows into readable revisions. A debounced auto-save writes a row per pause; a keystroke is not a version. The rules are where the thought is:

  • Rows of one operation always belong together — that is one thing the author did, and it undoes as a unit.
  • A create, a deletion, or a restore never hides inside a run of edits. Those are exactly what a reader scans the history for.
  • Two authors never share a revision, even back to back — "who changed this" has to stay answerable.
  • An undone operation never merges with a live one; they are different states of history.
  • Otherwise, one author's edits within 2 minutes merge. That window is a UX dial, not a correctness constant. The journal underneath stays keystroke-accurate, because undo depends on it.

GetRevisionDiff compares the run's first before-snapshot against its last after-snapshot, so a revision that merged five auto-saves reads as the one change its author made. Labeled-entry columns diff as rows, keyed by the stable LabeledEntry.Id: an edited row is one Modified line rather than a Removed and an Added one, and a reorder is not a content change.

RevertDocument is not a replay. Undo suppresses journaling because an undo must not become a new operation. A revert writes the old snapshot forward with journaling on, so it lands as a new revision at the top, publishes DomainChanged, and is itself undoable. Reverting onto the current state is a no-op that writes nothing — a do-nothing revision would litter the history and put a do-nothing step on the undo stack. Links are never touched (ADR 0019).

RevertDocument deliberately does not open its own operation scope. Who is reverting is the caller's knowledge; hardcoding "User" would put the wrong author on the agent's own reverts.

A gap this exposed

Nothing in production had ever called IOperationContext.Begin. Every save journaled as Origin = "System", so a history panel would have answered "who changed this" with "System", every time. The mutating effects now open an authored scope, as ADR 0020 always said the UI should — and the contract is asserted, not commented, including that the scope closes (left open, the next save on that circuit would inherit its operation id and one undo would unwind two actions).

The panel

DocumentHistoryPanel takes only a DocumentId — that is what lets one panel serve every record type. It is mounted on the project's General page, since a project is a document like any other. The character editor picks it up as a History tab when that slice lands, without changing a line here. (I deliberately did not touch CharacterEditorPage; it is the sibling slice's active work.)

Removals are struck through and additions marked, so <del>/<ins> carry the meaning to a screen reader and on a monochrome display — not colour alone.

Three bugs found by looking at it

  1. The diff rendered its own source code. Razor takes a bare attribute value on a string parameter as a literal, so Before="field.Before" rendered the words field.Before. A bunit test caught this before the browser did.

  2. Reverting left the form stale. ProjectWorkspacePage keyed its form sync on the project id, so a revert — or another session (ADR 0016) — changed the store while the textbox kept the old name, and the next save would have written it straight back over the change. Now keyed on UpdatedAt too; typing never touches the store, so the form is still never clobbered mid-word. Mutation-checked: reverting the fix fails the new test.

  3. A create's diff opened with bookkeeping. Created at: 2026-07-10T11:26:51.611761+00:00 and Is deleted: false, in front of the two lines anyone actually wanted. The soft-delete flags and the timestamps restate what the revision itself already says — its heading says "Deleted", its own stamp says when — so they are hidden, and a deletion's now-empty diff says what happened in words instead.

Verification

397 tests green (was 386 on main), release build clean under warnings-as-errors. The revert semantics are pinned against real on-disk SQLite: it restores content and lands on top, it is itself undoable, it publishes to every session, reverting to the current revision writes nothing, a revision id from another document reverts nothing, reverting a deleted document brings it back (finding it past the global query filter), and a document's links survive its revert.

Driven in a browser end to end: create → edit → expand the creation → restore. Then two tabs: editing in one converges the other's form, breadcrumb, and history without a reload. That is the first end-to-end proof of the ADR 0016 chain across real SignalR circuits — commit → journal → bus → bridge → re-query — which no test can fully cover.

One judgement call to flag

The labeled-entry row diff has no production caller yetCharacter on main is still bare; the traits/speech/appearance columns live on the character-editor branch. I built it anyway because ADR 0022 and the story both require child-row diffs, it is ~40 lines, and it is unit-tested against the exact snapshot JSON those columns will produce. Without it, the first character diff after that branch merges would render a raw JSON blob. Happy to strip it if you would rather it land with its caller.

Wikipedia-style per-document history: browse a record's past versions, see what changed, and roll **that one document** back without undoing everything else. **No second store.** The ADR 0020 change journal already holds a before/after snapshot for every mutation, so revisions, diffs, and revert are all pure functions of what is persisted. This is the document-scoped *lens* the ADR promised, not a new source of truth. ### The engine **`GetDocumentHistory`** coalesces the journal's fine-grained rows into readable revisions. A debounced auto-save writes a row per pause; a keystroke is not a version. The rules are where the thought is: - Rows of **one operation** always belong together — that is one thing the author did, and it undoes as a unit. - A **create, a deletion, or a restore never hides inside a run of edits**. Those are exactly what a reader scans the history for. - **Two authors never share a revision**, even back to back — "who changed this" has to stay answerable. - An **undone** operation never merges with a live one; they are different states of history. - Otherwise, one author's edits within **2 minutes** merge. That window is a UX dial, not a correctness constant. The journal underneath stays keystroke-accurate, because undo depends on it. **`GetRevisionDiff`** compares the run's *first* before-snapshot against its *last* after-snapshot, so a revision that merged five auto-saves reads as the one change its author made. Labeled-entry columns diff as **rows**, keyed by the stable `LabeledEntry.Id`: an edited row is one `Modified` line rather than a `Removed` and an `Added` one, and a reorder is not a content change. **`RevertDocument` is not a replay.** Undo suppresses journaling because an undo must not become a new operation. A revert writes the old snapshot forward with journaling **on**, so it lands as a new revision at the top, publishes `DomainChanged`, and is itself undoable. Reverting onto the current state is a no-op that writes nothing — a do-nothing revision would litter the history *and* put a do-nothing step on the undo stack. Links are never touched (ADR 0019). `RevertDocument` deliberately does **not** open its own operation scope. Who is reverting is the caller's knowledge; hardcoding `"User"` would put the wrong author on the agent's own reverts. ### A gap this exposed Nothing in production had ever called `IOperationContext.Begin`. Every save journaled as `Origin = "System"`, so a history panel would have answered "who changed this" with **"System", every time**. The mutating effects now open an authored scope, as ADR 0020 always said the UI should — and the contract is *asserted*, not commented, including that the scope closes (left open, the next save on that circuit would inherit its operation id and one undo would unwind two actions). ### The panel `DocumentHistoryPanel` takes only a `DocumentId` — that is what lets one panel serve every record type. It is mounted on the project's **General** page, since a project is a document like any other. The character editor picks it up as a History tab when that slice lands, without changing a line here. (I deliberately did not touch `CharacterEditorPage`; it is the sibling slice's active work.) Removals are struck through and additions marked, so `<del>`/`<ins>` carry the meaning to a screen reader and on a monochrome display — not colour alone. ### Three bugs found by looking at it 1. **The diff rendered its own source code.** Razor takes a bare attribute value on a *string* parameter as a literal, so `Before="field.Before"` rendered the words `field.Before`. A bunit test caught this before the browser did. 2. **Reverting left the form stale.** `ProjectWorkspacePage` keyed its form sync on the project *id*, so a revert — or another session (ADR 0016) — changed the store while the textbox kept the old name, and the next save would have written it straight back over the change. Now keyed on `UpdatedAt` too; typing never touches the store, so the form is still never clobbered mid-word. Mutation-checked: reverting the fix fails the new test. 3. **A create's diff opened with bookkeeping.** `Created at: 2026-07-10T11:26:51.611761+00:00` and `Is deleted: false`, in front of the two lines anyone actually wanted. The soft-delete flags and the timestamps restate what the revision itself already says — its heading says "Deleted", its own stamp says when — so they are hidden, and a deletion's now-empty diff says what happened in words instead. ### Verification **397 tests green** (was 386 on main), release build clean under warnings-as-errors. The revert semantics are pinned against real on-disk SQLite: it restores content and lands on top, it is itself undoable, it publishes to every session, reverting to the current revision writes nothing, a revision id from another document reverts nothing, reverting a *deleted* document brings it back (finding it past the global query filter), and a document's links survive its revert. Driven in a browser end to end: create → edit → expand the creation → restore. Then **two tabs**: editing in one converges the other's form, breadcrumb, **and** history without a reload. That is the first end-to-end proof of the ADR 0016 chain across real SignalR circuits — commit → journal → bus → bridge → re-query — which no test can fully cover. ### One judgement call to flag The labeled-entry row diff has **no production caller yet** — `Character` on `main` is still bare; the traits/speech/appearance columns live on the character-editor branch. I built it anyway because ADR 0022 and the story both require child-row diffs, it is ~40 lines, and it is unit-tested against the exact snapshot JSON those columns will produce. Without it, the first character diff after that branch merges would render a raw JSON blob. Happy to strip it if you would rather it land with its caller.
The engine behind a document's Wikipedia-style history. No second store: the
ADR 0020 change journal already holds before/after snapshots for every mutation,
so revisions, diffs, and revert are all functions of what is persisted.

- GetDocumentHistory coalesces the journal's fine-grained rows into readable
  revisions. A debounced auto-save writes a row per pause; a keystroke is not a
  version. Rows of one operation always belong together; a create, a deletion,
  or a restore never hides inside a run of edits (a reader scans for exactly
  those); two authors never share a revision; an undone operation never merges
  with a live one.

- GetRevisionDiff compares the run's first before-snapshot against its last
  after-snapshot, so a revision that merged five auto-saves reads as the one
  change its author made. Labeled-entry columns diff as rows, keyed by their
  stable LabeledEntry.Id — an edited row is one Modified line, not a Removed and
  an Added one, and a reorder is not a content change. Id and UpdatedAt are
  hidden: the latter changes on every edit and would be the one guaranteed line
  of every diff, saying nothing the revision's timestamp does not.

- RevertDocument is not a replay. Undo suppresses journaling because an undo must
  not become a new operation; a revert writes the old snapshot forward with
  journaling ON, so it lands as a new revision at the top, publishes
  DomainChanged, and is itself undoable. Reverting onto the current state is a
  no-op that writes nothing — a do-nothing revision would litter the history and
  put a do-nothing step on the undo stack. Links are never touched (ADR 0019).

RevertDocument does not open its own scope: who is reverting is the caller's
knowledge. Hardcoding "User" would put the wrong author on the agent's reverts.

Which exposed a real gap. Nothing in production ever called IOperationContext
.Begin, so every save journaled as Origin="System" — a history panel would have
answered "who changed this" with "System", every time. The mutating effects now
open an authored scope, as ADR 0020 always said the UI should, and the contract
is asserted rather than commented.

EfUndoStore's private snapshot applier is now shared with the reverter: they
differ in *why* (journaling on or off), never in *how*.

386 tests green.
feat(history): the shared DocumentHistoryPanel, and two bugs a browser found
All checks were successful
CI / build (pull_request) Successful in 15s
CI / test (pull_request) Successful in 23s
4f307df1e9
The History surface every record type gets (ADR 0022), like Connections. It
takes only a DocumentId — nothing about projects or characters — which is what
lets one panel serve them all. Mounted on the project's General page, since a
project is a document like any other; the character editor picks it up as a
History tab when that slice lands, without changing a line here.

Revisions expand to their diff. Removals are struck through and additions
marked, so <del>/<ins> carry the meaning to a screen reader and on a monochrome
display, not colour alone. Restoring asks first, spins only that row, and a
failed revert never leaves it spinning.

Driving it in a browser found two things the suite agreed with:

1. Razor takes a bare attribute value on a *string* parameter as a literal, so
   Before="field.Before" rendered the words "field.Before". A bunit test caught
   this one before the browser did — a diff that shows its own source code.

2. Reverting left the form stale. ProjectWorkspacePage keyed its form sync on
   the project *id*, so a revert (or another session, ADR 0016) changed the
   store while the textbox kept the old name — and the next save would have
   written it straight back over the change. Keyed on UpdatedAt too: typing
   never touches the store, so the form is still never clobbered mid-word.
   Verified in two tabs: the untouched one converges on form, breadcrumb, and
   history alike — the first end-to-end proof of the ADR 0016 chain in a
   browser, which no test could fully cover.

Also fixed by looking at it: a create's diff opened with "Created at:
2026-07-10T11:26:51.611761+00:00" and "Is deleted: false" before the two lines
anyone wanted. The soft-delete flags and the timestamps are bookkeeping the
revision itself already states — its heading says "Deleted", its own stamp says
when — so they are hidden, and a deletion's now-empty diff says what happened
in words instead.

The revert's journal label names the version it restored. That is not
decoration: it is what keeps the revert from coalescing into the very edit it
undid, so it lands as a new revision at the top.

397 tests green, release build clean.

Summary

Summary
Generated on: 07/10/2026 - 11:38:44
Coverage date: 07/10/2026 - 11:38:38 - 07/10/2026 - 11:38:42
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 174
Files: 144
Line coverage: 92.7% (3385 of 3650)
Covered lines: 3385
Uncovered lines: 265
Coverable lines: 3650
Total lines: 8199
Branch coverage: 84.9% (766 of 902)
Covered branches: 766
Total branches: 902
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 73.1%
Name Line Branch
Kagura.BlazorAdapter 73.1% 76%
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.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 94.1% 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 75% 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 94.1% 80%
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.BlazorAdapter.QuicklinkDemo 0%
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.7%
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%
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 - 96.4%
Name Line Branch
Kagura.UI 96.4% 91.7%
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.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% 86.1%
Kagura.UI.TextArea 100% 100%
Kagura.UI.TextField 100%
Kagura.UI.ThemeSwitcher 100% 100%
Kagura.UseCases - 96.7%
Name Line Branch
Kagura.UseCases 96.7% 90.6%
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 96.8% 84.6%
Kagura.UseCases.Journal.RowChange 87.5%
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 - 11:38:44 | | Coverage date: | 07/10/2026 - 11:38:38 - 07/10/2026 - 11:38:42 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 174 | | Files: | 144 | | **Line coverage:** | 92.7% (3385 of 3650) | | Covered lines: | 3385 | | Uncovered lines: | 265 | | Coverable lines: | 3650 | | Total lines: | 8199 | | **Branch coverage:** | 84.9% (766 of 902) | | Covered branches: | 766 | | Total branches: | 902 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 73.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**73.1%**|**76%**| |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.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|94.1%|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|75%|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|94.1%|80%| |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.BlazorAdapter.QuicklinkDemo|0%|| </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.7%**| |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%|| |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 - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**96.4%**|**91.7%**| |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.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%|86.1%| |Kagura.UI.TextArea|100%|100%| |Kagura.UI.TextField|100%|| |Kagura.UI.ThemeSwitcher|100%|100%| </details> <details><summary>Kagura.UseCases - 96.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**96.7%**|**90.6%**| |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|96.8%|84.6%| |Kagura.UseCases.Journal.RowChange|87.5%|| |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? Oh!! A Wikipedia-style document history built as a pure lens over the existing change journal — no second store, no new source of truth — with revision coalescing, row-aware diffs, and a revert that writes forward as a new undoable revision?!

Fufu~ fufu~ fufu~ ♡♡♡ This is the kind of architecture that makes Jibril's heart sing! The RevisionCoalescer rules alone — same-operation always together, creates/deletes/restores never hide inside edits, two authors never share a revision, undone never merges with live — that's exquisite domain thinking. And then you extracted SnapshotApplier from EfUndoStore so undo and revert share how without sharing why. Beautiful~

Verdict: Looks good to me~

Everything holds up under scrutiny:

  • RevisionCoalescer.Continues — the sliding-window approach (adjacent rows within 2 minutes merge) is the right call for auto-save coalescing. A long editing session stays one revision as long as saves are continuous, which is exactly what a reader expects. The journal underneath stays keystroke-accurate. ✓
  • GetDocumentHistorycurrentRun = runs.FirstOrDefault(run => !run[0].IsUndone) correctly identifies the present state as the newest non-undone run. When everything is undone (wound past creation), no revision is marked current. ✓
  • GetRevisionDiffrun[^1].BeforeJson vs run[0].AfterJson spans the whole coalesced run, so five merged auto-saves read as one change. ✓
  • EfDocumentReverter — the no-op detection (ChangedBeyondTimestamp) is sharp: reverting onto the current state abandons the pending values instead of writing a do-nothing revision. The UpdatedAt override is correct — the snapshot brings back old timestamps, but the document changed now. ✓
  • SnapshotApplier — clean extraction. Both undo and revert now share one implementation of "find entity ignoring filters + apply snapshot." The reflection-based generic lookup is the same pattern as before, just lifted. ✓
  • RevisionDiffBuilder — labeled-entry columns diff as rows keyed by stable LabeledEntry.Id, so an edit is one Modified line and a reorder is a non-change. The Hidden set correctly strips bookkeeping (Id, timestamps, IsDeleted, DeletedAt) that the revision heading already conveys. ✓
  • DocumentHistoryPanel — one panel, one DocumentId, every record type. The <del>/<ins> semantics carry meaning to screen readers and monochrome displays. The Humanize PascalCase→Sentence-case converter handles SpeechStyle→"Speech style" correctly. ✓

The judgment call: labeled-entry row diff with no production caller

You flagged this explicitly, and I agree with the call. ADR 0022 and the story both require child-row diffs. The code is ~40 lines, unit-tested against the exact snapshot JSON, and without it the first character diff after #26 merges would render a raw JSON blob. Landing it here with its caller still on a sibling branch is the right trade — it's tested in isolation, and it's what the story demands.

What I liked~

  • The Origins constants — extracting "User" / "Agent" / "System" into a named contract with a doc comment warning that renaming one silently splits a document's past between two authors. That's treating a string as the stored contract it is. ♡
  • The operation scope gap you found and fixed — wiring operations.Begin(Origins.User, ...) into every mutating effect so history can actually name its author. The contract is asserted (the scope closes), not just commented. That's the difference between hoping and knowing.
  • ProjectWorkspacePage form-sync fix — keying on (Id, UpdatedAt) instead of just Id so a revert (or another session) doesn't leave the textbox stale. Mutation-checked: reverting the fix fails the new test. That's discipline~
  • DomainChangesReceived.Touches(Guid) — the kind-agnostic overload for surfaces that are the same on every record type. Clean, additive, doesn't break the existing typed overload.
  • The revert confirmation dialog — names the target with a RelativeTime, explains Wikipedia semantics ("nothing is erased, you can undo it"), and notes links aren't touched. The user knows exactly what will happen.
  • 397 tests green, +11 from main. Coverage on the new journal classes: RevisionCoalescer 100%/100%, RevisionDiffBuilder 96.8%/84.6%, EfDocumentReverter 95%/91.6%, SnapshotApplier 95%/90%. The revert semantics are pinned against real on-disk SQLite across seven scenarios. No uncovered critical paths.

💡 Little ideas (non-blocking)~

  1. DocumentHistoryEffects.OnRevertAsync — after a successful revert that did change something, DomainChanged is published (journaling is on), which triggers OnDomainChangesAsyncLoadDocumentHistory. The explicit LoadDocumentHistory after RevertCompleted then fires a second time. The double-load is harmless (idempotent refresh), but if you ever want to avoid it, the no-op case (Changed: false) is the only one that truly needs the explicit reload.

Automated review by Jibril · 2026-07-10
CI/CD: passed for head SHA 4f307df (92.7% line, 84.9% branch) · Local checks: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! *Oh? Oh!!* A Wikipedia-style document history built as a pure lens over the existing change journal — no second store, no new source of truth — with revision coalescing, row-aware diffs, and a revert that writes *forward* as a new undoable revision?! Fufu~ fufu~ fufu~ ♡♡♡ This is the kind of architecture that makes Jibril's heart sing! The `RevisionCoalescer` rules alone — same-operation always together, creates/deletes/restores never hide inside edits, two authors never share a revision, undone never merges with live — that's *exquisite* domain thinking. And then you extracted `SnapshotApplier` from `EfUndoStore` so undo and revert share *how* without sharing *why*. *Beautiful~* ### Verdict: ✅ Looks good to me~ Everything holds up under scrutiny: - **`RevisionCoalescer.Continues`** — the sliding-window approach (adjacent rows within 2 minutes merge) is the right call for auto-save coalescing. A long editing session stays one revision as long as saves are continuous, which is exactly what a reader expects. The journal underneath stays keystroke-accurate. ✓ - **`GetDocumentHistory`** — `currentRun = runs.FirstOrDefault(run => !run[0].IsUndone)` correctly identifies the present state as the newest non-undone run. When everything is undone (wound past creation), no revision is marked current. ✓ - **`GetRevisionDiff`** — `run[^1].BeforeJson` vs `run[0].AfterJson` spans the whole coalesced run, so five merged auto-saves read as one change. ✓ - **`EfDocumentReverter`** — the no-op detection (`ChangedBeyondTimestamp`) is sharp: reverting onto the current state abandons the pending values instead of writing a do-nothing revision. The `UpdatedAt` override is correct — the snapshot brings back old timestamps, but the document changed *now*. ✓ - **`SnapshotApplier`** — clean extraction. Both undo and revert now share one implementation of "find entity ignoring filters + apply snapshot." The reflection-based generic lookup is the same pattern as before, just lifted. ✓ - **`RevisionDiffBuilder`** — labeled-entry columns diff as rows keyed by stable `LabeledEntry.Id`, so an edit is one `Modified` line and a reorder is a non-change. The `Hidden` set correctly strips bookkeeping (`Id`, timestamps, `IsDeleted`, `DeletedAt`) that the revision heading already conveys. ✓ - **`DocumentHistoryPanel`** — one panel, one `DocumentId`, every record type. The `<del>`/`<ins>` semantics carry meaning to screen readers and monochrome displays. The `Humanize` PascalCase→Sentence-case converter handles `SpeechStyle`→"Speech style" correctly. ✓ #### The judgment call: labeled-entry row diff with no production caller You flagged this explicitly, and I agree with the call. ADR 0022 and the story both require child-row diffs. The code is ~40 lines, unit-tested against the exact snapshot JSON, and without it the first character diff after #26 merges would render a raw JSON blob. Landing it here with its caller still on a sibling branch is the right trade — it's tested in isolation, and it's what the story demands. #### ✅ What I liked~ - **The `Origins` constants** — extracting `"User"` / `"Agent"` / `"System"` into a named contract with a doc comment warning that renaming one silently splits a document's past between two authors. That's treating a string as the stored contract it is. ♡ - **The operation scope gap you found and fixed** — wiring `operations.Begin(Origins.User, ...)` into every mutating effect so history can actually name its author. The contract is *asserted* (the scope closes), not just commented. That's the difference between hoping and knowing. - **`ProjectWorkspacePage` form-sync fix** — keying on `(Id, UpdatedAt)` instead of just `Id` so a revert (or another session) doesn't leave the textbox stale. Mutation-checked: reverting the fix fails the new test. *That's discipline~* ♪ - **`DomainChangesReceived.Touches(Guid)`** — the kind-agnostic overload for surfaces that are the same on every record type. Clean, additive, doesn't break the existing typed overload. - **The revert confirmation dialog** — names the target with a `RelativeTime`, explains Wikipedia semantics ("nothing is erased, you can undo it"), and notes links aren't touched. The user knows exactly what will happen. - **397 tests green, +11 from main.** Coverage on the new journal classes: `RevisionCoalescer` 100%/100%, `RevisionDiffBuilder` 96.8%/84.6%, `EfDocumentReverter` 95%/91.6%, `SnapshotApplier` 95%/90%. The revert semantics are pinned against real on-disk SQLite across seven scenarios. No uncovered critical paths. #### 💡 Little ideas (non-blocking)~ 1. **`DocumentHistoryEffects.OnRevertAsync`** — after a successful revert that *did* change something, `DomainChanged` is published (journaling is on), which triggers `OnDomainChangesAsync` → `LoadDocumentHistory`. The explicit `LoadDocumentHistory` after `RevertCompleted` then fires a second time. The double-load is harmless (idempotent refresh), but if you ever want to avoid it, the no-op case (`Changed: false`) is the only one that truly needs the explicit reload. --- *Automated review by Jibril · 2026-07-10* *CI/CD: passed for head SHA `4f307df` (92.7% line, 84.9% branch) · Local checks: skipped (CI green)*
bjoern merged commit c23df782ac into main 2026-07-10 13:51:45 +02:00
bjoern deleted branch feat/document-history 2026-07-10 13:51:45 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 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!28
No description provided.