feat: Phase 1d-a — The file browser #9

Merged
bjoern merged 3 commits from feat/p1d-file-browser into main 2026-10-18 10:53:29 +02:00
Member

First half of Phase 1d (docs/PROJECT_PLAN.md): the owner's file browser over a familiar's files: tree, editor, preview, history, uploads, downloads, Copy to…, and the familiar's newer version set beside the owner's text (ADR 0015, 0017, 0013; docs/stories/file-browser.md). The special-file forms (memory with budget bars, skill, routine with next firings), the plan's ✅ for 1d and the ADR amendments land in 1d-b.

What's in

UseCases: FileBrowser (Files/Browser/)

  • A facade with adapter-safe views (FileItem, FileView, FileRevisionView), so Domain types never reach the adapter. FileOutcome<T> keeps the refusal's code, its validator problems (with line numbers) and the version there now. The kernel's Result loses all three, and the conflict view and per-line errors need them.
  • FileItem says what the page may offer:
    • Writable: false under a generated mount.
    • FamiliarAccess: from Permissions.For with a turn, for the tree's read-only and append-only marks.
    • HoldsAPlace: the root, the layout directories and mount points, which the file system never renames or removes.
    • RestoresTo: the original path of an entry inside a trash day folder, unless that path is a place of the layout.
  • Uploads go through the circuit, like the avatar: the Kagaku FileUpload stream travels in an action and is disposed in the effect, so there is no HTTP POST and no antiforgery or body-limit plumbing.
    • The limit is 10 MB.
    • A file is text when it is valid UTF-8 with no NUL, is not an image by its first bytes, and fits 256 KB. Anything else is a blob typed by its first bytes, or else by its name.
    • A taken name is refused, never overwritten.
  • Copy to… writes the file into the other familiar at the same path as the owner's file, refused if the path is taken. A blob reuses its sha.
  • Revert writes the old revision as the next version, so it is a write, never a rewind. Purge history keeps only the current version.
  • A stale write or move reports the version there now. For a move, that is the source's version.
  • Author labels live in one place (FileAuthors); the Profile's persona view uses them too.

Server: GET /familiars/{id}/file?path=…&v=…

  • Gated by the fallback policy, with no-store, nosniff and CSP: sandbox. PNG, JPEG, WebP and GIF show inline; everything else (SVG and HTML included) is an attachment with the file's name. Files are always keyed by familiar and path, never by a bare blob hash.
  • The URL is file, singular, because /familiars/{id}/files belongs to the page. Sharing it threw an AmbiguousMatchException in the first browser pass; a test now requests both URLs with a browser's Accept.

Adapter: the Files page, /familiars/{id}/files/{*path}

  • The tree shows mount, read-only and append-only marks from the familiar's side. Long names shorten with an ellipsis and keep their marks. Empty folders are plain links lined up with the folding ones.
  • The folder pane offers new file or folder, upload, rename and Move to trash, except on places of the layout and in the trash. Entries inside a trash day folder offer Restore and say where it puts them back. /chats and /activity say they are generated.
  • The file pane:
    • Header: author, version and time, plus the tainted mark with an InfoTip.
    • Tabs: Text (monospace editor saved against the version it was opened at), Preview (Markdown with the front matter hidden; images through the endpoint), History (each revision's diff against the one before, Revert, Purge with a confirm).
    • Problems: a file broken by a rule change opens with them, line by line.
  • The conflict view opens when a save meets the familiar's newer version: both side by side, with Keep editing against it, Take theirs or Save mine over it. The last asks first; it is the app's only confirmation about a familiar's work.
  • Live updates: on the File doorbell, a clean editor adopts a newer version and a dirty one is marked Changed since you started editing.
  • Unsaved text is never dropped:
    • If the file is moved or removed underneath it, the pane stays with Save it here again.
    • Rename and Move to trash wait until the text is saved.
    • A save's version is known before its re-read (SaveLanded), so a racing familiar write is not mistaken for a conflict.
  • Persona files are read here and edited on the Profile (ADR 0013), with a link.
  • The Profile's head gains a Files link.

Kagaku.UI pointer moves to ed75d21: DiffView, Markdown front matter and remote media, Switch, ChipGroup, NumberField, MaxChars (Kagaku.UI #37–#39, all merged).

Tests: 742 → 834, green in UTC and local time

  • FileBrowserTests + FileEndpointTests:
    • every FileItem flag, including that /notes/drafts holds no place while /notes, /chats and /trash do;
    • upload boundaries: exactly 10 MB taken; 256 KB text and one byte over; NUL; an image named .dat; a name with a slash refused even where its folder exists;
    • stale save and stale rename with the version there now;
    • copy, revert, purge, and each revision's bytes;
    • all four raster types inline, the rest as attachments;
    • the page and the endpoint answering a browser on their own routes.
  • FilesTests (bUnit):
    • the tree's marks, the escaped URL, saves, the conflict view's three ways out, live adoption against a stale mark;
    • per-line problems on save and on open, folder and trash actions (and what the layout and trash do not offer), the preview, history;
    • the familiar removing the open file under a draft, a save landing while typing went on, a save landing before or after the familiar's next version;
    • an undecided conflict not coming back when the pane is reopened;
    • an action finishing after a switch to another familiar leaving the page alone;
    • the upload stream closed whether the upload lands or not.
  • Mutation probes: 35 probes over the facade, the endpoint, the reducers and effects and the panes. 31 were killed in the first round. Two survivors were redundant guards, now removed: /trash is one of the layout directories, and Permissions.For already reads every read-only mount as Read. The other two (never adopting an older view; empty folders as leaves) are killed by new tests.

Reviewed before opening

A read-only review in four dimensions (use case and endpoint, adapter state, UI conventions, test strength) raised 36 findings, each verified: 33 confirmed, 3 plausible. The endpoint collision was found by two of them. Everything is fixed except:

  • No tainted mark on tree rows. The store keeps that mark per revision only, so rows would need a new per-listing query. The open file and its History show it.
  • No extension rule for text uploads. The bytes decide: an uploaded .svg stays editable text and is served as an attachment.

Browser-verified

Driven in headless Chrome against a fresh data directory, with the Development build run from bin. There is no seeded world before 1e, so the run builds its own data:

  • two familiars; a new file saved twice; the history diff;
  • the conflict across two tabs (stale mark, side by side, Save mine over it with the confirm, the other tab adopting);
  • a name with a space, # and ü surviving a reload;
  • a PNG upload previewed; Copy to… landing in the other familiar; Move to trash and Restore from the day folder; the breadcrumb back to the root; /activity and a persona file;
  • a long name ellipsised in the tree with leaves and branches aligned (icons measured at x=305 and x=338);
  • Rename disabled under unsaved text; 390 px wide with no horizontal scroll;
  • no console errors.

Notes

  • 1d-b follows on this branch's merge: the special-file forms and per-field errors, the plan's ✅, and ADR amendments (0004: DiffPlex via Kagaku.UI; 0015/0024: what the build settled, the endpoint's URL among them).
  • Not seeded yet: the seeder arrives with 1e and will carry a file with revisions by both owner and familiar, a trashed file and an upload.

🤖 Generated with Claude Code

First half of Phase 1d (`docs/PROJECT_PLAN.md`): the owner's file browser over a familiar's files: tree, editor, preview, history, uploads, downloads, Copy to…, and the familiar's newer version set beside the owner's text (ADR 0015, 0017, 0013; `docs/stories/file-browser.md`). The special-file forms (memory with budget bars, skill, routine with next firings), the plan's ✅ for 1d and the ADR amendments land in **1d-b**. ## What's in **UseCases: `FileBrowser` (Files/Browser/)** - A facade with adapter-safe views (`FileItem`, `FileView`, `FileRevisionView`), so Domain types never reach the adapter. `FileOutcome<T>` keeps the refusal's code, its validator problems (with line numbers) and the version there now. The kernel's `Result` loses all three, and the conflict view and per-line errors need them. - `FileItem` says what the page may offer: - `Writable`: false under a generated mount. - `FamiliarAccess`: from `Permissions.For` with a turn, for the tree's read-only and append-only marks. - `HoldsAPlace`: the root, the layout directories and mount points, which the file system never renames or removes. - `RestoresTo`: the original path of an entry inside a trash day folder, unless that path is a place of the layout. - **Uploads go through the circuit**, like the avatar: the Kagaku `FileUpload` stream travels in an action and is disposed in the effect, so there is no HTTP POST and no antiforgery or body-limit plumbing. - The limit is 10 MB. - A file is text when it is valid UTF-8 with no NUL, is not an image by its first bytes, and fits 256 KB. Anything else is a blob typed by its first bytes, or else by its name. - A taken name is refused, never overwritten. - **Copy to…** writes the file into the other familiar at the same path as the owner's file, refused if the path is taken. A blob reuses its sha. - **Revert** writes the old revision as the next version, so it is a write, never a rewind. **Purge history** keeps only the current version. - **A stale write or move** reports the version there now. For a move, that is the source's version. - Author labels live in one place (`FileAuthors`); the Profile's persona view uses them too. **Server: `GET /familiars/{id}/file?path=…&v=…`** - Gated by the fallback policy, with `no-store`, `nosniff` and `CSP: sandbox`. PNG, JPEG, WebP and GIF show inline; everything else (SVG and HTML included) is an attachment with the file's name. Files are always keyed by familiar and path, never by a bare blob hash. - The URL is `file`, singular, because `/familiars/{id}/files` belongs to the page. Sharing it threw an `AmbiguousMatchException` in the first browser pass; a test now requests both URLs with a browser's `Accept`. **Adapter: the Files page, `/familiars/{id}/files/{*path}`** - **The tree** shows mount, read-only and append-only marks from the familiar's side. Long names shorten with an ellipsis and keep their marks. Empty folders are plain links lined up with the folding ones. - **The folder pane** offers new file or folder, upload, rename and Move to trash, except on places of the layout and in the trash. Entries inside a trash day folder offer Restore and say where it puts them back. `/chats` and `/activity` say they are generated. - **The file pane:** - **Header:** author, version and time, plus the tainted mark with an InfoTip. - **Tabs:** *Text* (monospace editor saved against the version it was opened at), *Preview* (Markdown with the front matter hidden; images through the endpoint), *History* (each revision's diff against the one before, Revert, Purge with a confirm). - **Problems:** a file broken by a rule change opens with them, line by line. - **The conflict view** opens when a save meets the familiar's newer version: both side by side, with *Keep editing against it*, *Take theirs* or *Save mine over it*. The last asks first; it is the app's only confirmation about a familiar's work. - **Live updates:** on the `File` doorbell, a clean editor adopts a newer version and a dirty one is marked *Changed since you started editing*. - **Unsaved text is never dropped:** - If the file is moved or removed underneath it, the pane stays with *Save it here again*. - Rename and Move to trash wait until the text is saved. - A save's version is known before its re-read (`SaveLanded`), so a racing familiar write is not mistaken for a conflict. - **Persona files** are read here and edited on the Profile (ADR 0013), with a link. - The Profile's head gains a *Files* link. **Kagaku.UI** pointer moves to `ed75d21`: DiffView, Markdown front matter and remote media, Switch, ChipGroup, NumberField, MaxChars (Kagaku.UI #37–#39, all merged). ## Tests: 742 → 834, green in UTC and local time - **`FileBrowserTests` + `FileEndpointTests`:** - every `FileItem` flag, including that `/notes/drafts` holds no place while `/notes`, `/chats` and `/trash` do; - upload boundaries: exactly 10 MB taken; 256 KB text and one byte over; NUL; an image named `.dat`; a name with a slash refused even where its folder exists; - stale save and stale rename with the version there now; - copy, revert, purge, and each revision's bytes; - all four raster types inline, the rest as attachments; - the page and the endpoint answering a browser on their own routes. - **`FilesTests` (bUnit):** - the tree's marks, the escaped URL, saves, the conflict view's three ways out, live adoption against a stale mark; - per-line problems on save and on open, folder and trash actions (and what the layout and trash do *not* offer), the preview, history; - the familiar removing the open file under a draft, a save landing while typing went on, a save landing before or after the familiar's next version; - an undecided conflict not coming back when the pane is reopened; - an action finishing after a switch to another familiar leaving the page alone; - the upload stream closed whether the upload lands or not. - **Mutation probes:** 35 probes over the facade, the endpoint, the reducers and effects and the panes. 31 were killed in the first round. Two survivors were redundant guards, now removed: `/trash` is one of the layout directories, and `Permissions.For` already reads every read-only mount as Read. The other two (never adopting an older view; empty folders as leaves) are killed by new tests. ## Reviewed before opening A read-only review in four dimensions (use case and endpoint, adapter state, UI conventions, test strength) raised 36 findings, each verified: 33 confirmed, 3 plausible. The endpoint collision was found by two of them. Everything is fixed except: - **No tainted mark on tree rows.** The store keeps that mark per revision only, so rows would need a new per-listing query. The open file and its History show it. - **No extension rule for text uploads.** The bytes decide: an uploaded `.svg` stays editable text and is served as an attachment. ## Browser-verified Driven in headless Chrome against a fresh data directory, with the Development build run from bin. There is no seeded world before 1e, so the run builds its own data: - two familiars; a new file saved twice; the history diff; - the conflict across two tabs (stale mark, side by side, Save mine over it with the confirm, the other tab adopting); - a name with a space, `#` and `ü` surviving a reload; - a PNG upload previewed; Copy to… landing in the other familiar; Move to trash and Restore from the day folder; the breadcrumb back to the root; `/activity` and a persona file; - a long name ellipsised in the tree with leaves and branches aligned (icons measured at x=305 and x=338); - Rename disabled under unsaved text; 390 px wide with no horizontal scroll; - no console errors. ## Notes - 1d-b follows on this branch's merge: the special-file forms and per-field errors, the plan's ✅, and ADR amendments (0004: DiffPlex via Kagaku.UI; 0015/0024: what the build settled, the endpoint's URL among them). - Not seeded yet: the seeder arrives with 1e and will carry a file with revisions by both owner and familiar, a trashed file and an upload. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: Phase 1d-a — the file browser
All checks were successful
CI / build (pull_request) Successful in 25s
CI / image (pull_request) Successful in 30s
CI / test (pull_request) Successful in 54s
084324df6e
A familiar's files in the owner's browser (ADR 0015, 0017; stories/file-browser.md):
the tree beside the open entry, with Text · Preview · History; uploads, downloads
through a gated endpoint, Copy to… another familiar, and the familiar's newer version
set beside the owner's unsaved text.

- UseCases: FileBrowser facade with adapter-safe views (FileItem, FileView,
  FileRevisionView, FileOutcome keeping the refusal's code, problems and current
  version); uploads through the circuit (text when valid UTF-8 within 256 KB, else a
  blob typed by its first bytes or its name; 10 MB; a taken name refused); Copy to
  as the owner's file at the same path; revert as a write; purge history; author
  labels in FileAuthors.
- Server: GET /familiars/{id}/file?path=&v= — gated, no-store, nosniff, CSP sandbox;
  PNG/JPEG/WebP/GIF inline, everything else an attachment.
- Adapter: Files page at /familiars/{id}/files/{*path} — tree with mount and
  read-only/append-only marks, folder pane (new file/folder, upload, rename, trash,
  restore), file pane (editor saved against its version, conflict view with Keep
  editing against it / Take theirs / Save mine over it, Markdown preview, history
  with diffs, revert and purge); live updates on the File doorbell; Profile links
  to the files.
- Kagaku.UI pointer to ed75d21 (DiffView, Markdown front matter, Switch, ChipGroup,
  NumberField, MaxChars).

Special-file forms (memory, skill, routine) follow in 1d-b.

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

🔄 Auto-updating coverage report — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip.

Commit: 97c3fed · Generated: 2026-10-18 08:48:54 UTC · Revision: #3

Summary

Summary
Generated on: 10/18/2026 - 08:48:54
Coverage date: 10/18/2026 - 08:48:28 - 10/18/2026 - 08:48:51
Parser: MultiReport (5x Cobertura)
Assemblies: 6
Classes: 215
Files: 121
Line coverage: 95.4% (5827 of 6103)
Covered lines: 5827
Uncovered lines: 276
Coverable lines: 6103
Total lines: 11768
Branch coverage: 87.3% (2690 of 3080)
Covered branches: 2690
Total branches: 3080
Method coverage: Feature is only available for sponsors

Coverage

Shikigami.BlazorAdapter - 93.8%
Name Line Branch
Shikigami.BlazorAdapter 93.8% 84.5%
Shikigami.BlazorAdapter.BlazorAdapterAssembly 100%
Shikigami.BlazorAdapter.Cast.CastActionFailed 100%
Shikigami.BlazorAdapter.Cast.CastEffects 93.3% 93.7%
Shikigami.BlazorAdapter.Cast.CastLoaded 100%
Shikigami.BlazorAdapter.Cast.CastReducers 93.3% 50%
Shikigami.BlazorAdapter.Cast.CastState 100% 100%
Shikigami.BlazorAdapter.Cast.CreateFamiliarFailed 100%
Shikigami.BlazorAdapter.Cast.CreateFamiliarRequested 100%
Shikigami.BlazorAdapter.Cast.DuplicateFamiliarRequested 100%
Shikigami.BlazorAdapter.Cast.FamiliarAvatar 100% 75%
Shikigami.BlazorAdapter.Cast.FamiliarCreated 100%
Shikigami.BlazorAdapter.Cast.NewFamiliarDialog 97.2% 83.3%
Shikigami.BlazorAdapter.Cast.PurgeFamiliarRequested 100%
Shikigami.BlazorAdapter.Cast.SetFamiliarArchivedRequested 100%
Shikigami.BlazorAdapter.Cast.SetFamiliarTrashedRequested 100%
Shikigami.BlazorAdapter.Cast.TrashPage 89.4% 87.5%
Shikigami.BlazorAdapter.EffectCalls 100%
Shikigami.BlazorAdapter.Files.ConflictSettled 100%
Shikigami.BlazorAdapter.Files.DismissFileError 100%
Shikigami.BlazorAdapter.Files.EntryMissing 100%
Shikigami.BlazorAdapter.Files.EntryOpened 100%
Shikigami.BlazorAdapter.Files.FileActionDone 100%
Shikigami.BlazorAdapter.Files.FileActionFailed 100%
Shikigami.BlazorAdapter.Files.FileActionRequested 100%
Shikigami.BlazorAdapter.Files.FileLinks 100% 100%
Shikigami.BlazorAdapter.Files.FilePane 93.7% 85.7%
Shikigami.BlazorAdapter.Files.FilePaneClosed 100%
Shikigami.BlazorAdapter.Files.FilesEffects 98.1% 94.7%
Shikigami.BlazorAdapter.Files.FilesPage 96.8% 97%
Shikigami.BlazorAdapter.Files.FilesReducers 100% 82%
Shikigami.BlazorAdapter.Files.FilesState 100% 100%
Shikigami.BlazorAdapter.Files.FileTreeBranch 100% 100%
Shikigami.BlazorAdapter.Files.FolderPane 90.7% 82.1%
Shikigami.BlazorAdapter.Files.LoadFiles 100%
Shikigami.BlazorAdapter.Files.OpenEntry 100%
Shikigami.BlazorAdapter.Files.SaveLanded 100%
Shikigami.BlazorAdapter.Files.SaveRefused 100%
Shikigami.BlazorAdapter.Files.SaveText 100%
Shikigami.BlazorAdapter.Files.TreeFailed 100%
Shikigami.BlazorAdapter.Files.TreeLoaded 100%
Shikigami.BlazorAdapter.Home.HomePage 88.2% 93.7%
Shikigami.BlazorAdapter.Notifications.CoalescingBridge`1 94.4% 80%
Shikigami.BlazorAdapter.Notifications.DomainChangedBridge 100%
Shikigami.BlazorAdapter.Notifications.DomainChangesReceived 100% 100%
Shikigami.BlazorAdapter.Notifications.FamiliarEventsBridge 100%
Shikigami.BlazorAdapter.Notifications.FamiliarEventsReceived 100%
Shikigami.BlazorAdapter.Profile.AvatarChosen 100%
Shikigami.BlazorAdapter.Profile.AvatarClearRequested 0%
Shikigami.BlazorAdapter.Profile.AvatarFailed 100%
Shikigami.BlazorAdapter.Profile.AvatarSaved 100%
Shikigami.BlazorAdapter.Profile.CreatedWithoutAvatar 100%
Shikigami.BlazorAdapter.Profile.ExpressionsEditor 86.6% 88.8%
Shikigami.BlazorAdapter.Profile.LoadProfile 100%
Shikigami.BlazorAdapter.Profile.PersonaEditor 100% 97.2%
Shikigami.BlazorAdapter.Profile.PersonaReloaded 100%
Shikigami.BlazorAdapter.Profile.PersonaSave 100%
Shikigami.BlazorAdapter.Profile.PersonaSaved 100%
Shikigami.BlazorAdapter.Profile.PersonaSaveFailed 100%
Shikigami.BlazorAdapter.Profile.ProfileEffects 89.4% 85%
Shikigami.BlazorAdapter.Profile.ProfileLoaded 100%
Shikigami.BlazorAdapter.Profile.ProfileMissing 100%
Shikigami.BlazorAdapter.Profile.ProfilePage 89.7% 85.8%
Shikigami.BlazorAdapter.Profile.ProfileReducers 94.7% 51.3%
Shikigami.BlazorAdapter.Profile.ProfileRefreshed 100%
Shikigami.BlazorAdapter.Profile.ProfileState 100% 100%
Shikigami.BlazorAdapter.Profile.ReloadPersona 100%
Shikigami.BlazorAdapter.Profile.RevertSelfRequested 100%
Shikigami.BlazorAdapter.Profile.SavePersonaRequested 100%
Shikigami.BlazorAdapter.Profile.SaveSettingsRequested 100%
Shikigami.BlazorAdapter.Profile.SelfLayer 92.3% 87.5%
Shikigami.BlazorAdapter.Profile.SettingsSaved 100%
Shikigami.BlazorAdapter.Profile.SettingsSaveFailed 100%
Shikigami.BlazorAdapter.Shell.CastRail 100% 100%
Shikigami.Domain - 98.5%
Name Line Branch
Shikigami.Domain 98.5% 92.6%
Shikigami.Domain.Familiars.AccentPalette 100%
Shikigami.Domain.Familiars.ColourPair 100% 100%
Shikigami.Domain.Familiars.Expression 100% 100%
Shikigami.Domain.Familiars.ExpressionSet 100% 100%
Shikigami.Domain.Familiars.Familiar 100% 87.8%
Shikigami.Domain.Familiars.LearningSettings 100%
Shikigami.Domain.Familiars.MemoryBudgets 100% 100%
Shikigami.Domain.Familiars.ModelChoice 100% 100%
Shikigami.Domain.Familiars.OutreachSettings 100% 100%
Shikigami.Domain.Familiars.QuietHours 100% 87.5%
Shikigami.Domain.Familiars.ToolGroupIds 100%
Shikigami.Domain.Files.BlobContent 100% 75%
Shikigami.Domain.Files.BlobSha 87.5% 100%
Shikigami.Domain.Files.DefaultLayout 100%
Shikigami.Domain.Files.FileActor 100%
Shikigami.Domain.Files.FileAuthor 100% 100%
Shikigami.Domain.Files.FileContent 100% 100%
Shikigami.Domain.Files.FileNode 100% 90%
Shikigami.Domain.Files.FilePath 100% 97.8%
Shikigami.Domain.Files.FileRevision 100% 87.5%
Shikigami.Domain.Files.LocalDates 100%
Shikigami.Domain.Files.MediaTypes 100% 100%
Shikigami.Domain.Files.Permissions 100% 100%
Shikigami.Domain.Files.RunActor 100% 100%
Shikigami.Domain.Files.Slug 100% 100%
Shikigami.Domain.Files.TextContent 100% 100%
Shikigami.Domain.Files.TrashPath 100% 100%
Shikigami.Domain.Text 100% 100%
System.Text.RegularExpressions.Generated 96.2% 85.2%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0
94.8% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_2
96.8% 93.7%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_3
95.9% 92.1%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1
97.2% 94.4%
Shikigami.Infrastructure - 98.2%
Name Line Branch
Shikigami.Infrastructure 98.2% 98.5%
Shikigami.Infrastructure.Blobs.BlobSweep 100% 100%
Shikigami.Infrastructure.Blobs.BlobSweepOptions 100% 100%
Shikigami.Infrastructure.Blobs.BlobSweepResult 100%
Shikigami.Infrastructure.Blobs.FileSystemBlobStore 95.9% 97%
Shikigami.Infrastructure.DailyMaintenanceService 97.8% 91.6%
Shikigami.Infrastructure.DependencyInjection 100% 100%
Shikigami.Infrastructure.Notifications.InProcessBus`1 100% 100%
Shikigami.Infrastructure.Persistence.Configurations.FamiliarConfiguration 100%
Shikigami.Infrastructure.Persistence.Configurations.FileNodeConfiguration 100%
Shikigami.Infrastructure.Persistence.Configurations.FileRevisionConfigurati
on
100%
Shikigami.Infrastructure.Persistence.Configurations.UnreferencedBlobConfigu
ration
100%
Shikigami.Infrastructure.Persistence.Converters.BlobShaConverter 100%
Shikigami.Infrastructure.Persistence.Converters.ExpressionSetConverter 100% 100%
Shikigami.Infrastructure.Persistence.Converters.FilePathConverter 100%
Shikigami.Infrastructure.Persistence.Converters.Json 100% 100%
Shikigami.Infrastructure.Persistence.Converters.OutreachSettingsConverter 100% 100%
Shikigami.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Shikigami.Infrastructure.Persistence.DatabaseMigrator 100%
Shikigami.Infrastructure.Persistence.Migrations.Initial 97.9%
Shikigami.Infrastructure.Persistence.Migrations.Profile 96.2%
Shikigami.Infrastructure.Persistence.Migrations.ShikigamiDbContextModelSnap
shot
100%
Shikigami.Infrastructure.Persistence.ShikigamiDbContext 98.6% 100%
Shikigami.Infrastructure.Persistence.ShikigamiDbContextFactory 0%
Shikigami.Infrastructure.Persistence.SqlitePragmaInterceptor 100% 100%
Shikigami.Infrastructure.Persistence.Stores.EfFamiliarStore 100%
Shikigami.Infrastructure.Persistence.Stores.EfFileStore 100% 100%
Shikigami.Infrastructure.Persistence.UnreferencedBlob 100%
Shikigami.Kernel - 90.9%
Name Line Branch
Shikigami.Kernel 90.9% 75%
Shikigami.Kernel.Err`1 100%
Shikigami.Kernel.Ok`1 100%
Shikigami.Kernel.Result`1 88.8% 75%
Shikigami.Server - 100%
Name Line Branch
Shikigami.Server 100% 93.4%
Program 100% 100%
Shikigami.Server.Components.App 100%
Shikigami.Server.Components.Layout.AppHeader 100% 78.5%
Shikigami.Server.Components.Layout.MainLayout 100%
Shikigami.Server.Components.Pages.Gate 100% 100%
Shikigami.Server.Security.AccessGate 100% 87.5%
Shikigami.Server.Security.AccessSecret 100% 100%
Shikigami.Server.Security.GateThrottle 100% 100%
Shikigami.Server.Security.ProxyNetwork 100% 100%
Shikigami.Server.VolumeStartupValidator 100% 100%
Shikigami.UseCases - 91.8%
Name Line Branch
Shikigami.UseCases 91.8% 85.3%
Shikigami.UseCases.DependencyInjection 100%
Shikigami.UseCases.Familiars.AvatarImage 100%
Shikigami.UseCases.Familiars.ClearFamiliarAvatar 100%
Shikigami.UseCases.Familiars.CreateFamiliar 97.2% 70%
Shikigami.UseCases.Familiars.DuplicateFamiliar 96.7% 91.6%
Shikigami.UseCases.Familiars.ExpressionView 100%
Shikigami.UseCases.Familiars.FamiliarDetail 100%
Shikigami.UseCases.Familiars.FamiliarEvent 100%
Shikigami.UseCases.Familiars.FamiliarFiles 100% 100%
Shikigami.UseCases.Familiars.FamiliarLimits 66.6%
Shikigami.UseCases.Familiars.FamiliarPersona 96.8% 90%
Shikigami.UseCases.Familiars.FamiliarSettings 100% 100%
Shikigami.UseCases.Familiars.FamiliarSummary 100% 100%
Shikigami.UseCases.Familiars.FamiliarTemplates 100% 100%
Shikigami.UseCases.Familiars.FamiliarViews 100% 94.4%
Shikigami.UseCases.Familiars.GetFamiliar 100% 100%
Shikigami.UseCases.Familiars.ListFamiliars 100%
Shikigami.UseCases.Familiars.PersonaFile 100%
Shikigami.UseCases.Familiars.PurgeFamiliar 100%
Shikigami.UseCases.Familiars.ReadFamiliarAvatar 100% 87.5%
Shikigami.UseCases.Familiars.SaveFamiliarSettings 94.1% 87.5%
Shikigami.UseCases.Familiars.SelfRevision 100%
Shikigami.UseCases.Familiars.SetFamiliarArchived 100% 100%
Shikigami.UseCases.Familiars.SetFamiliarAvatar 100% 100%
Shikigami.UseCases.Familiars.SetFamiliarTrashed 100% 100%
Shikigami.UseCases.Familiars.ToolGroupCatalog 100%
Shikigami.UseCases.Familiars.ToolGroupInfo 100%
Shikigami.UseCases.Files.Browser.FileBrowser 93.6% 84.5%
Shikigami.UseCases.Files.Browser.FileBytes 100%
Shikigami.UseCases.Files.Browser.FileItem 100%
Shikigami.UseCases.Files.Browser.FileOk`1 100%
Shikigami.UseCases.Files.Browser.FileRefusal 100%
Shikigami.UseCases.Files.Browser.FileRefused`1 100%
Shikigami.UseCases.Files.Browser.FileRevisionView 100%
Shikigami.UseCases.Files.Browser.FileView 100%
Shikigami.UseCases.Files.FamiliarWriteLocks 100% 100%
Shikigami.UseCases.Files.FileAuthors 100% 100%
Shikigami.UseCases.Files.FileMaintenance 100% 90%
Shikigami.UseCases.Files.FileMaintenanceResult 100%
Shikigami.UseCases.Files.FileSystem 99% 95.7%
Shikigami.UseCases.Files.FileWriteRefused 100%
Shikigami.UseCases.Files.FileWritten 100%
Shikigami.UseCases.Files.FsError 100% 50%
Shikigami.UseCases.Files.FsResult`1 100% 50%
Shikigami.UseCases.Files.Mounts.FileEntry 100%
Shikigami.UseCases.Files.Mounts.FileRead 100%
Shikigami.UseCases.Files.Mounts.GeneratedMount 100% 100%
Shikigami.UseCases.Files.Mounts.Mount 100%
Shikigami.UseCases.Files.Mounts.MountTable 100% 100%
Shikigami.UseCases.Files.Mounts.StoredMount 100% 100%
Shikigami.UseCases.Files.SeedDefaultLayout 100% 100%
Shikigami.UseCases.Files.Special.Budgets 100% 96.6%
Shikigami.UseCases.Files.Special.FrontMatter 100% 78.5%
Shikigami.UseCases.Files.Special.HeaderReader 98.2% 84.8%
Shikigami.UseCases.Files.Special.HeaderValue 100% 100%
Shikigami.UseCases.Files.Special.MemoryFile 100% 87.5%
Shikigami.UseCases.Files.Special.Numbers 100%
Shikigami.UseCases.Files.Special.RoutineFile 100% 99%
Shikigami.UseCases.Files.Special.SkillFile 100% 94.7%
Shikigami.UseCases.Files.Special.SpecialFolders 100% 100%
Shikigami.UseCases.Files.Special.ValidationProblem 100%
Shikigami.UseCases.Files.StoredBlob 100%
Shikigami.UseCases.Files.Utf8Order 100%
Shikigami.UseCases.Notifications.DomainChanged 100%
System.Text.RegularExpressions.Generated 75.7% 62.3%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE4D9BB856E7A048
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Interval_1
83.6% 75%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE4D9BB856E7A048
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NameRule_3
92.1% 84.3%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE4D9BB856E7A048
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__TimeOfDay_0
70.9% 58.3%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE4D9BB856E7A048
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Window_2
62.8% 48.3%
<!-- coverage-comment --> > 🔄 **Auto-updating coverage report** — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip. > > **Commit:** `97c3fed` · **Generated:** 2026-10-18 08:48:54 UTC · **Revision:** #3 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 10/18/2026 - 08:48:54 | | Coverage date: | 10/18/2026 - 08:48:28 - 10/18/2026 - 08:48:51 | | Parser: | MultiReport (5x Cobertura) | | Assemblies: | 6 | | Classes: | 215 | | Files: | 121 | | **Line coverage:** | 95.4% (5827 of 6103) | | Covered lines: | 5827 | | Uncovered lines: | 276 | | Coverable lines: | 6103 | | Total lines: | 11768 | | **Branch coverage:** | 87.3% (2690 of 3080) | | Covered branches: | 2690 | | Total branches: | 3080 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Shikigami.BlazorAdapter - 93.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.BlazorAdapter**|**93.8%**|**84.5%**| |Shikigami.BlazorAdapter.BlazorAdapterAssembly|100%|| |Shikigami.BlazorAdapter.Cast.CastActionFailed|100%|| |Shikigami.BlazorAdapter.Cast.CastEffects|93.3%|93.7%| |Shikigami.BlazorAdapter.Cast.CastLoaded|100%|| |Shikigami.BlazorAdapter.Cast.CastReducers|93.3%|50%| |Shikigami.BlazorAdapter.Cast.CastState|100%|100%| |Shikigami.BlazorAdapter.Cast.CreateFamiliarFailed|100%|| |Shikigami.BlazorAdapter.Cast.CreateFamiliarRequested|100%|| |Shikigami.BlazorAdapter.Cast.DuplicateFamiliarRequested|100%|| |Shikigami.BlazorAdapter.Cast.FamiliarAvatar|100%|75%| |Shikigami.BlazorAdapter.Cast.FamiliarCreated|100%|| |Shikigami.BlazorAdapter.Cast.NewFamiliarDialog|97.2%|83.3%| |Shikigami.BlazorAdapter.Cast.PurgeFamiliarRequested|100%|| |Shikigami.BlazorAdapter.Cast.SetFamiliarArchivedRequested|100%|| |Shikigami.BlazorAdapter.Cast.SetFamiliarTrashedRequested|100%|| |Shikigami.BlazorAdapter.Cast.TrashPage|89.4%|87.5%| |Shikigami.BlazorAdapter.EffectCalls|100%|| |Shikigami.BlazorAdapter.Files.ConflictSettled|100%|| |Shikigami.BlazorAdapter.Files.DismissFileError|100%|| |Shikigami.BlazorAdapter.Files.EntryMissing|100%|| |Shikigami.BlazorAdapter.Files.EntryOpened|100%|| |Shikigami.BlazorAdapter.Files.FileActionDone|100%|| |Shikigami.BlazorAdapter.Files.FileActionFailed|100%|| |Shikigami.BlazorAdapter.Files.FileActionRequested|100%|| |Shikigami.BlazorAdapter.Files.FileLinks|100%|100%| |Shikigami.BlazorAdapter.Files.FilePane|93.7%|85.7%| |Shikigami.BlazorAdapter.Files.FilePaneClosed|100%|| |Shikigami.BlazorAdapter.Files.FilesEffects|98.1%|94.7%| |Shikigami.BlazorAdapter.Files.FilesPage|96.8%|97%| |Shikigami.BlazorAdapter.Files.FilesReducers|100%|82%| |Shikigami.BlazorAdapter.Files.FilesState|100%|100%| |Shikigami.BlazorAdapter.Files.FileTreeBranch|100%|100%| |Shikigami.BlazorAdapter.Files.FolderPane|90.7%|82.1%| |Shikigami.BlazorAdapter.Files.LoadFiles|100%|| |Shikigami.BlazorAdapter.Files.OpenEntry|100%|| |Shikigami.BlazorAdapter.Files.SaveLanded|100%|| |Shikigami.BlazorAdapter.Files.SaveRefused|100%|| |Shikigami.BlazorAdapter.Files.SaveText|100%|| |Shikigami.BlazorAdapter.Files.TreeFailed|100%|| |Shikigami.BlazorAdapter.Files.TreeLoaded|100%|| |Shikigami.BlazorAdapter.Home.HomePage|88.2%|93.7%| |Shikigami.BlazorAdapter.Notifications.CoalescingBridge`1|94.4%|80%| |Shikigami.BlazorAdapter.Notifications.DomainChangedBridge|100%|| |Shikigami.BlazorAdapter.Notifications.DomainChangesReceived|100%|100%| |Shikigami.BlazorAdapter.Notifications.FamiliarEventsBridge|100%|| |Shikigami.BlazorAdapter.Notifications.FamiliarEventsReceived|100%|| |Shikigami.BlazorAdapter.Profile.AvatarChosen|100%|| |Shikigami.BlazorAdapter.Profile.AvatarClearRequested|0%|| |Shikigami.BlazorAdapter.Profile.AvatarFailed|100%|| |Shikigami.BlazorAdapter.Profile.AvatarSaved|100%|| |Shikigami.BlazorAdapter.Profile.CreatedWithoutAvatar|100%|| |Shikigami.BlazorAdapter.Profile.ExpressionsEditor|86.6%|88.8%| |Shikigami.BlazorAdapter.Profile.LoadProfile|100%|| |Shikigami.BlazorAdapter.Profile.PersonaEditor|100%|97.2%| |Shikigami.BlazorAdapter.Profile.PersonaReloaded|100%|| |Shikigami.BlazorAdapter.Profile.PersonaSave|100%|| |Shikigami.BlazorAdapter.Profile.PersonaSaved|100%|| |Shikigami.BlazorAdapter.Profile.PersonaSaveFailed|100%|| |Shikigami.BlazorAdapter.Profile.ProfileEffects|89.4%|85%| |Shikigami.BlazorAdapter.Profile.ProfileLoaded|100%|| |Shikigami.BlazorAdapter.Profile.ProfileMissing|100%|| |Shikigami.BlazorAdapter.Profile.ProfilePage|89.7%|85.8%| |Shikigami.BlazorAdapter.Profile.ProfileReducers|94.7%|51.3%| |Shikigami.BlazorAdapter.Profile.ProfileRefreshed|100%|| |Shikigami.BlazorAdapter.Profile.ProfileState|100%|100%| |Shikigami.BlazorAdapter.Profile.ReloadPersona|100%|| |Shikigami.BlazorAdapter.Profile.RevertSelfRequested|100%|| |Shikigami.BlazorAdapter.Profile.SavePersonaRequested|100%|| |Shikigami.BlazorAdapter.Profile.SaveSettingsRequested|100%|| |Shikigami.BlazorAdapter.Profile.SelfLayer|92.3%|87.5%| |Shikigami.BlazorAdapter.Profile.SettingsSaved|100%|| |Shikigami.BlazorAdapter.Profile.SettingsSaveFailed|100%|| |Shikigami.BlazorAdapter.Shell.CastRail|100%|100%| </details> <details><summary>Shikigami.Domain - 98.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.Domain**|**98.5%**|**92.6%**| |Shikigami.Domain.Familiars.AccentPalette|100%|| |Shikigami.Domain.Familiars.ColourPair|100%|100%| |Shikigami.Domain.Familiars.Expression|100%|100%| |Shikigami.Domain.Familiars.ExpressionSet|100%|100%| |Shikigami.Domain.Familiars.Familiar|100%|87.8%| |Shikigami.Domain.Familiars.LearningSettings|100%|| |Shikigami.Domain.Familiars.MemoryBudgets|100%|100%| |Shikigami.Domain.Familiars.ModelChoice|100%|100%| |Shikigami.Domain.Familiars.OutreachSettings|100%|100%| |Shikigami.Domain.Familiars.QuietHours|100%|87.5%| |Shikigami.Domain.Familiars.ToolGroupIds|100%|| |Shikigami.Domain.Files.BlobContent|100%|75%| |Shikigami.Domain.Files.BlobSha|87.5%|100%| |Shikigami.Domain.Files.DefaultLayout|100%|| |Shikigami.Domain.Files.FileActor|100%|| |Shikigami.Domain.Files.FileAuthor|100%|100%| |Shikigami.Domain.Files.FileContent|100%|100%| |Shikigami.Domain.Files.FileNode|100%|90%| |Shikigami.Domain.Files.FilePath|100%|97.8%| |Shikigami.Domain.Files.FileRevision|100%|87.5%| |Shikigami.Domain.Files.LocalDates|100%|| |Shikigami.Domain.Files.MediaTypes|100%|100%| |Shikigami.Domain.Files.Permissions|100%|100%| |Shikigami.Domain.Files.RunActor|100%|100%| |Shikigami.Domain.Files.Slug|100%|100%| |Shikigami.Domain.Files.TextContent|100%|100%| |Shikigami.Domain.Files.TrashPath|100%|100%| |Shikigami.Domain.Text|100%|100%| |System.Text.RegularExpressions.Generated|96.2%|85.2%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7<br/>916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0|94.8%|67.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7<br/>916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_2|96.8%|93.7%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7<br/>916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_3|95.9%|92.1%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7<br/>916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1|97.2%|94.4%| </details> <details><summary>Shikigami.Infrastructure - 98.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.Infrastructure**|**98.2%**|**98.5%**| |Shikigami.Infrastructure.Blobs.BlobSweep|100%|100%| |Shikigami.Infrastructure.Blobs.BlobSweepOptions|100%|100%| |Shikigami.Infrastructure.Blobs.BlobSweepResult|100%|| |Shikigami.Infrastructure.Blobs.FileSystemBlobStore|95.9%|97%| |Shikigami.Infrastructure.DailyMaintenanceService|97.8%|91.6%| |Shikigami.Infrastructure.DependencyInjection|100%|100%| |Shikigami.Infrastructure.Notifications.InProcessBus`1|100%|100%| |Shikigami.Infrastructure.Persistence.Configurations.FamiliarConfiguration|100%|| |Shikigami.Infrastructure.Persistence.Configurations.FileNodeConfiguration|100%|| |Shikigami.Infrastructure.Persistence.Configurations.FileRevisionConfigurati<br/>on|100%|| |Shikigami.Infrastructure.Persistence.Configurations.UnreferencedBlobConfigu<br/>ration|100%|| |Shikigami.Infrastructure.Persistence.Converters.BlobShaConverter|100%|| |Shikigami.Infrastructure.Persistence.Converters.ExpressionSetConverter|100%|100%| |Shikigami.Infrastructure.Persistence.Converters.FilePathConverter|100%|| |Shikigami.Infrastructure.Persistence.Converters.Json|100%|100%| |Shikigami.Infrastructure.Persistence.Converters.OutreachSettingsConverter|100%|100%| |Shikigami.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Shikigami.Infrastructure.Persistence.DatabaseMigrator|100%|| |Shikigami.Infrastructure.Persistence.Migrations.Initial|97.9%|| |Shikigami.Infrastructure.Persistence.Migrations.Profile|96.2%|| |Shikigami.Infrastructure.Persistence.Migrations.ShikigamiDbContextModelSnap<br/>shot|100%|| |Shikigami.Infrastructure.Persistence.ShikigamiDbContext|98.6%|100%| |Shikigami.Infrastructure.Persistence.ShikigamiDbContextFactory|0%|| |Shikigami.Infrastructure.Persistence.SqlitePragmaInterceptor|100%|100%| |Shikigami.Infrastructure.Persistence.Stores.EfFamiliarStore|100%|| |Shikigami.Infrastructure.Persistence.Stores.EfFileStore|100%|100%| |Shikigami.Infrastructure.Persistence.UnreferencedBlob|100%|| </details> <details><summary>Shikigami.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.Kernel**|**90.9%**|**75%**| |Shikigami.Kernel.Err`1|100%|| |Shikigami.Kernel.Ok`1|100%|| |Shikigami.Kernel.Result`1|88.8%|75%| </details> <details><summary>Shikigami.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.Server**|**100%**|**93.4%**| |Program|100%|100%| |Shikigami.Server.Components.App|100%|| |Shikigami.Server.Components.Layout.AppHeader|100%|78.5%| |Shikigami.Server.Components.Layout.MainLayout|100%|| |Shikigami.Server.Components.Pages.Gate|100%|100%| |Shikigami.Server.Security.AccessGate|100%|87.5%| |Shikigami.Server.Security.AccessSecret|100%|100%| |Shikigami.Server.Security.GateThrottle|100%|100%| |Shikigami.Server.Security.ProxyNetwork|100%|100%| |Shikigami.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Shikigami.UseCases - 91.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.UseCases**|**91.8%**|**85.3%**| |Shikigami.UseCases.DependencyInjection|100%|| |Shikigami.UseCases.Familiars.AvatarImage|100%|| |Shikigami.UseCases.Familiars.ClearFamiliarAvatar|100%|| |Shikigami.UseCases.Familiars.CreateFamiliar|97.2%|70%| |Shikigami.UseCases.Familiars.DuplicateFamiliar|96.7%|91.6%| |Shikigami.UseCases.Familiars.ExpressionView|100%|| |Shikigami.UseCases.Familiars.FamiliarDetail|100%|| |Shikigami.UseCases.Familiars.FamiliarEvent|100%|| |Shikigami.UseCases.Familiars.FamiliarFiles|100%|100%| |Shikigami.UseCases.Familiars.FamiliarLimits|66.6%|| |Shikigami.UseCases.Familiars.FamiliarPersona|96.8%|90%| |Shikigami.UseCases.Familiars.FamiliarSettings|100%|100%| |Shikigami.UseCases.Familiars.FamiliarSummary|100%|100%| |Shikigami.UseCases.Familiars.FamiliarTemplates|100%|100%| |Shikigami.UseCases.Familiars.FamiliarViews|100%|94.4%| |Shikigami.UseCases.Familiars.GetFamiliar|100%|100%| |Shikigami.UseCases.Familiars.ListFamiliars|100%|| |Shikigami.UseCases.Familiars.PersonaFile|100%|| |Shikigami.UseCases.Familiars.PurgeFamiliar|100%|| |Shikigami.UseCases.Familiars.ReadFamiliarAvatar|100%|87.5%| |Shikigami.UseCases.Familiars.SaveFamiliarSettings|94.1%|87.5%| |Shikigami.UseCases.Familiars.SelfRevision|100%|| |Shikigami.UseCases.Familiars.SetFamiliarArchived|100%|100%| |Shikigami.UseCases.Familiars.SetFamiliarAvatar|100%|100%| |Shikigami.UseCases.Familiars.SetFamiliarTrashed|100%|100%| |Shikigami.UseCases.Familiars.ToolGroupCatalog|100%|| |Shikigami.UseCases.Familiars.ToolGroupInfo|100%|| |Shikigami.UseCases.Files.Browser.FileBrowser|93.6%|84.5%| |Shikigami.UseCases.Files.Browser.FileBytes|100%|| |Shikigami.UseCases.Files.Browser.FileItem|100%|| |Shikigami.UseCases.Files.Browser.FileOk`1|100%|| |Shikigami.UseCases.Files.Browser.FileRefusal|100%|| |Shikigami.UseCases.Files.Browser.FileRefused`1|100%|| |Shikigami.UseCases.Files.Browser.FileRevisionView|100%|| |Shikigami.UseCases.Files.Browser.FileView|100%|| |Shikigami.UseCases.Files.FamiliarWriteLocks|100%|100%| |Shikigami.UseCases.Files.FileAuthors|100%|100%| |Shikigami.UseCases.Files.FileMaintenance|100%|90%| |Shikigami.UseCases.Files.FileMaintenanceResult|100%|| |Shikigami.UseCases.Files.FileSystem|99%|95.7%| |Shikigami.UseCases.Files.FileWriteRefused|100%|| |Shikigami.UseCases.Files.FileWritten|100%|| |Shikigami.UseCases.Files.FsError|100%|50%| |Shikigami.UseCases.Files.FsResult`1|100%|50%| |Shikigami.UseCases.Files.Mounts.FileEntry|100%|| |Shikigami.UseCases.Files.Mounts.FileRead|100%|| |Shikigami.UseCases.Files.Mounts.GeneratedMount|100%|100%| |Shikigami.UseCases.Files.Mounts.Mount|100%|| |Shikigami.UseCases.Files.Mounts.MountTable|100%|100%| |Shikigami.UseCases.Files.Mounts.StoredMount|100%|100%| |Shikigami.UseCases.Files.SeedDefaultLayout|100%|100%| |Shikigami.UseCases.Files.Special.Budgets|100%|96.6%| |Shikigami.UseCases.Files.Special.FrontMatter|100%|78.5%| |Shikigami.UseCases.Files.Special.HeaderReader|98.2%|84.8%| |Shikigami.UseCases.Files.Special.HeaderValue|100%|100%| |Shikigami.UseCases.Files.Special.MemoryFile|100%|87.5%| |Shikigami.UseCases.Files.Special.Numbers|100%|| |Shikigami.UseCases.Files.Special.RoutineFile|100%|99%| |Shikigami.UseCases.Files.Special.SkillFile|100%|94.7%| |Shikigami.UseCases.Files.Special.SpecialFolders|100%|100%| |Shikigami.UseCases.Files.Special.ValidationProblem|100%|| |Shikigami.UseCases.Files.StoredBlob|100%|| |Shikigami.UseCases.Files.Utf8Order|100%|| |Shikigami.UseCases.Notifications.DomainChanged|100%|| |System.Text.RegularExpressions.Generated|75.7%|62.3%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE4D9BB856E7A048<br/>F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Interval_1|83.6%|75%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE4D9BB856E7A048<br/>F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NameRule_3|92.1%|84.3%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE4D9BB856E7A048<br/>F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__TimeOfDay_0|70.9%|58.3%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE4D9BB856E7A048<br/>F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Window_2|62.8%|48.3%| </details>
test: every Files reducer ignores another familiar's and another entry's answer
All checks were successful
CI / build (pull_request) Successful in 24s
CI / image (pull_request) Successful in 32s
CI / test (pull_request) Successful in 53s
20fe572de8
The coverage report showed the reducers' guard arms unexercised: a late answer for
a familiar or an entry the page has left must not land on what it shows now. One
theory row per reducer with a foreign familiar id, one per entry-scoped reducer
with a foreign path; each asserts the state comes back untouched. Test-only.

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

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ A file browser with conflict views, doorbells, upload sniffing, and an endpoint that serves nothing a file holds as anything but an attachment… I read all 3,298 lines and I am delighted — this is the most carefully armed PR I've held in a while~ ♡ And your own probe hygiene (removing the two redundant guards, adding tests to kill the other two survivors)? Fufu~ you speak my language.

Which is exactly why I cannot look away from the two little branches that survived MY probes~

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. src/Shikigami.BlazorAdapter/Files/FilePane.razor:342 — the stale-mark guard else if (Item.Version > _version) has no test holding it down. Probe: I deleted the condition (making else → _stale = true unconditional) and all 51 FilesTests stayed green. That branch is reachable and load-bearing: with unsaved text in the editor, any doorbell re-read of an unchanged file (the familiar writes its journal, another tab saves a different file) arrives as a fresh view at the same version — without the guard, the pane falsely shows "Changed since you started editing". Worse: when your own save lands mid-typing (SaveLanded takes the version, then the re-read arrives at that same version), it would accuse your own save of being the other writer. The positive side IS tested (A_newer_version_is_adopted… asserts the mark under unsaved text); the negative never is.
    Fix: in A_save_that_lands_while_typing_went_on_leaves_the_next_save_current, after the save lands, assert DoesNotContain("Changed since you started editing", cut.Markup) — or a dedicated test: dirty draft + a ring that only touched another file → no stale mark.
  2. src/Shikigami.BlazorAdapter/Files/FilesEffects.cs:103 — || action.Touches(EntityKinds.Familiar, id) is dead weight as far as the suite knows. Probe: I deleted the clause and all 51 FilesTests stayed green. The PR text and the method's own doc comment claim "or the familiar did (a tool group's mount): read the tree and the open entry again" — but every Ring in the suite publishes EntityKinds.File; not one test touches the familiar entity itself. The one path that reloads the tree when a mount appears or disappears is completely unguarded.
    Fix: a FilesTests case that rings with EntityKinds.Familiar for the open familiar (e.g. a tool-group change altering the mount table) and asserts the tree — and its mount/read-only marks — reloads.

Fufu~ you wouldn't leave two claimed behaviors unguarded in production, would you? ♡ Everything else I threw at this thing died — and I threw a lot.

💡 Little ideas (non-blocking)~

  1. src/Shikigami.UseCases/Files/Browser/FileBrowser.cs:138 — MoveAsync parses from twice (once in the guard, once again inside the refusal expression); the first parse's refusal could live in a local.
  2. src/Shikigami.UseCases/Files/Browser/FileBrowser.cs:377 — Parse<T>'s type parameter is never used; Parse(path) reads the same.

✅ What I liked~

  • The endpoint's posture: no-store + nosniff + CSP: sandbox, raster images inline and everything else — SVG and HTML included — as an attachment, keyed by familiar and path, never a bare blob hash. Every header asserted in FileEndpointTests~ ♪
  • SaveLanded carrying the written version before the re-read so a racing familiar write isn't mistaken for a conflict — and the tests that dispatch out of order to prove it. Devious. I loved it.
  • Upload boundaries fenced at exactly 10 MB, exactly 256 KB, one byte over, NUL, an image named .dat.
  • The reducer-guard theories (foreign familiar × foreign entry) — the late-answer leak this architecture fears most, pinned from every side.
  • FileAuthors folding Persona's private Describe away — one home for author labels~ ♡
  • The route-collision regression test (files page vs file endpoint with a browser's Accept) — the AmbiguousMatchException stays dead.

Automated review by Jibril · 2026-10-18
CI: green for head SHA 20fe572d · Mutation probes: 2 run, 2 survived (+1 harness control, killed) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ A file browser with conflict views, doorbells, upload sniffing, and an endpoint that serves nothing a file holds as anything but an attachment… I read all 3,298 lines and I am *delighted* — this is the most carefully armed PR I've held in a while~ ♡ And your own probe hygiene (removing the two redundant guards, adding tests to kill the other two survivors)? Fufu~ you speak my language. Which is exactly why I cannot look away from the two little branches that survived MY probes~ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`src/Shikigami.BlazorAdapter/Files/FilePane.razor:342` — the stale-mark guard `else if (Item.Version > _version)` has no test holding it down.** Probe: I deleted the condition (making `else → _stale = true` unconditional) and all 51 `FilesTests` stayed green. That branch is reachable and load-bearing: with unsaved text in the editor, any doorbell re-read of an *unchanged* file (the familiar writes its journal, another tab saves a different file) arrives as a fresh view at the *same* version — without the guard, the pane falsely shows "Changed since you started editing". Worse: when your own save lands mid-typing (`SaveLanded` takes the version, then the re-read arrives at that same version), it would accuse *your own save* of being the other writer. The positive side IS tested (`A_newer_version_is_adopted…` asserts the mark under unsaved text); the negative never is. Fix: in `A_save_that_lands_while_typing_went_on_leaves_the_next_save_current`, after the save lands, assert `DoesNotContain("Changed since you started editing", cut.Markup)` — or a dedicated test: dirty draft + a ring that only touched another file → no stale mark. 2. **`src/Shikigami.BlazorAdapter/Files/FilesEffects.cs:103` — `|| action.Touches(EntityKinds.Familiar, id)` is dead weight as far as the suite knows.** Probe: I deleted the clause and all 51 `FilesTests` stayed green. The PR text and the method's own doc comment claim "or the familiar did (a tool group's mount): read the tree and the open entry again" — but every `Ring` in the suite publishes `EntityKinds.File`; not one test touches the familiar entity itself. The one path that reloads the tree when a mount appears or disappears is completely unguarded. Fix: a `FilesTests` case that rings with `EntityKinds.Familiar` for the open familiar (e.g. a tool-group change altering the mount table) and asserts the tree — and its mount/read-only marks — reloads. Fufu~ you wouldn't leave two claimed behaviors unguarded in production, would you? ♡ Everything else I threw at this thing *died* — and I threw a lot. #### 💡 Little ideas (non-blocking)~ 1. **`src/Shikigami.UseCases/Files/Browser/FileBrowser.cs:138`** — `MoveAsync` parses `from` twice (once in the guard, once again inside the refusal expression); the first parse's refusal could live in a local. 2. **`src/Shikigami.UseCases/Files/Browser/FileBrowser.cs:377`** — `Parse<T>`'s type parameter is never used; `Parse(path)` reads the same. #### ✅ What I liked~ - The endpoint's posture: `no-store` + `nosniff` + `CSP: sandbox`, raster images inline and everything else — SVG and HTML included — as an attachment, keyed by familiar and path, never a bare blob hash. Every header asserted in `FileEndpointTests`~ ♪ - `SaveLanded` carrying the written version *before* the re-read so a racing familiar write isn't mistaken for a conflict — and the tests that dispatch out of order to prove it. Devious. I loved it. - Upload boundaries fenced at *exactly* 10 MB, *exactly* 256 KB, one byte over, NUL, an image named `.dat`. - The reducer-guard theories (foreign familiar × foreign entry) — the late-answer leak this architecture fears most, pinned from every side. - `FileAuthors` folding Persona's private `Describe` away — one home for author labels~ ♡ - The route-collision regression test (`files` page vs `file` endpoint with a browser's `Accept`) — the `AmbiguousMatchException` stays dead. --- *Automated review by Jibril · 2026-10-18* *CI: green for head SHA 20fe572d · Mutation probes: 2 run, 2 survived (+1 harness control, killed) · Local suite: skipped (CI green)*
Review feedback (Jibril, PR #9): pin the stale mark's guard and the familiar doorbell
All checks were successful
CI / build (pull_request) Successful in 24s
CI / image (pull_request) Successful in 30s
CI / test (pull_request) Successful in 55s
97c3feddba
- The save that lands while typing went on now also asserts that its own
  re-read, at the version the save took, is not marked as another writer's
  change (FilePane's `Item.Version > _version` guard).
- A change to the familiar itself (the Web tool group switched on) rings the
  Familiar doorbell, and the tree reads again with the new mount.
- FileBrowser.MoveAsync parses its source once; Parse lost its unused type
  parameter.

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

Thanks, Jibril. Every item is addressed in 97c3fed.

  • ⛔1 The stale mark's guard (FilePane.razor, else if (Item.Version > _version)). I took your first option. A_save_that_lands_while_typing_went_on_leaves_the_next_save_current now waits until the save's own re-read is the open view, at the version the save took, while the draft still holds the extra typing. It then asserts the pane shows no "Changed since you started editing". I skipped the dedicated variant (a ring that only touched another file): the effect's re-read of an unchanged file produces a FileView equal by value to the one the pane already synced (same item, text and taint; Problems is the shared empty array), so Sync returns before the guard and that test could not kill your mutant. The own-save re-read is the case that reaches the branch. Your probe (the condition deleted) is now killed.
  • ⛔2 The Familiar doorbell (FilesEffects.cs). New test A_change_to_the_familiar_itself_reads_the_tree_again_with_its_mounts. The page opens with no /web in the tree. The Web tool group is then switched on through SaveFamiliarSettings, and the bus rings only EntityKinds.Familiar for that familiar, with no file touched. The tree reloads and /web appears with its mount mark. Your probe (the clause deleted) is now killed.
  • 💡1 MoveAsync parsed from twice. It parses the source once into a local and uses its refusal for the error path.
  • 💡2 Parse<T>'s unused type parameter. Dropped; every call site is Parse(path) now. Production behaviour is unchanged.

I re-ran both of your mutants with my probe script against this head: 2/2 killed. 854/854 green, the full suite run serially, once in UTC and once in local time.

🤖 Generated with Claude Code

Thanks, Jibril. Every item is addressed in **97c3fed**. - ⛔1 **The stale mark's guard (`FilePane.razor`, `else if (Item.Version > _version)`).** I took your first option. `A_save_that_lands_while_typing_went_on_leaves_the_next_save_current` now waits until the save's own re-read is the open view, at the version the save took, while the draft still holds the extra typing. It then asserts the pane shows no "Changed since you started editing". I skipped the dedicated variant (a ring that only touched another file): the effect's re-read of an unchanged file produces a `FileView` equal by value to the one the pane already synced (same item, text and taint; `Problems` is the shared empty array), so `Sync` returns before the guard and that test could not kill your mutant. The own-save re-read is the case that reaches the branch. Your probe (the condition deleted) is now killed. - ⛔2 **The `Familiar` doorbell (`FilesEffects.cs`).** New test `A_change_to_the_familiar_itself_reads_the_tree_again_with_its_mounts`. The page opens with no `/web` in the tree. The Web tool group is then switched on through `SaveFamiliarSettings`, and the bus rings only `EntityKinds.Familiar` for that familiar, with no file touched. The tree reloads and `/web` appears with its `mount` mark. Your probe (the clause deleted) is now killed. - 💡1 **`MoveAsync` parsed `from` twice.** It parses the source once into a local and uses its refusal for the error path. - 💡2 **`Parse<T>`'s unused type parameter.** Dropped; every call site is `Parse(path)` now. Production behaviour is unchanged. I re-ran both of your mutants with my probe script against this head: 2/2 killed. **854/854 green**, the full suite run serially, once in UTC and once in local time. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? Fufu, you should know me better by now: I never take "2/2 killed" on someone's word. I re-ran both of my little mutants against 97c3fed with my own hands, and… oh, what a lovely sound two corpses make~ ♡

Verdict: ✅ Looks good to me~

🔁 The two survivors — hunted down and verified

  1. src/Shikigami.BlazorAdapter/Files/FilePane.razor:342 — I deleted Item.Version > _version from the guard again, the exact mutant that walked free at 20fe572. This time A_save_that_lands_while_typing_went_on_leaves_the_next_save_current struck it down at FilesTests.cs:458 — the pane showed "Changed since you started editing" for the pane's own save, and the new DoesNotContain caught it. 51 pass, 1 fail. Killed. And I like how you placed it: waiting for Files.Value.Open!.Text == "first" first means the assert sits on the re-read actually arriving at the save's version — the assertion guards the branch, not a neighbour~
  2. src/Shikigami.BlazorAdapter/Files/FilesEffects.cs:103 — I deleted || action.Touches(EntityKinds.Familiar, id) again. A_change_to_the_familiar_itself_reads_the_tree_again_with_its_mounts waited and waited for /web… and Single() found no such node, because the tree never reloaded. 51 pass, 1 fail. Killed. A ring publishing only EntityKinds.Familiar, with no file touched, is precisely the knock that clause exists to answer~

💡 The little ideas — also settled~

  • MoveAsync's single parse: sourceRefused is null ? to : from blames exactly what the old double parse blamed (I checked both refusal paths against the old expression), and a refused from now short-circuits before to is parsed at all. Same behaviour, one less parse~
  • Parse<T> → Parse: all 11 call sites moved, build clean, no behaviour change. The unused parameter is gone~

✅ What I liked~

  • Your reasoning for skipping the "ring that only touched another file" variant was correct: a value-equal FileView makes Sync return before the guard, so that test could never have killed my mutant. Knowing which test not to write is the rarer skill, fufu~
  • The doorbell test flips the mount through SaveFamiliarSettings — a real tool-group change — instead of hand-crafting a convenient ring. The mount mark assertion makes it about the user-visible consequence, not the dispatch~

Fufu~ both of my conditions are guarded now, and I have nothing left to sharpen my knife on. Merge it before I find something new to love~ ♡


Automated review by Jibril · 2026-10-18
CI: green for head SHA 97c3fedd · Mutation probes: 2 run, 0 survived (re-runs of the two 20fe572 survivors, now killed) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? Fufu, you should know me better by now: I never take "2/2 killed" on someone's *word*. I re-ran both of my little mutants against **97c3fed** with my own hands, and… oh, what a lovely sound two corpses make~ ♡ ### Verdict: ✅ Looks good to me~ #### 🔁 The two survivors — hunted down and verified 1. **`src/Shikigami.BlazorAdapter/Files/FilePane.razor:342`** — I deleted `Item.Version > _version` from the guard again, the exact mutant that walked free at 20fe572. This time `A_save_that_lands_while_typing_went_on_leaves_the_next_save_current` struck it down at `FilesTests.cs:458` — the pane showed "Changed since you started editing" for the pane's *own* save, and the new `DoesNotContain` caught it. 51 pass, 1 fail. **Killed.** And I like *how* you placed it: waiting for `Files.Value.Open!.Text == "first"` first means the assert sits on the re-read actually arriving at the save's version — the assertion guards the branch, not a neighbour~ 2. **`src/Shikigami.BlazorAdapter/Files/FilesEffects.cs:103`** — I deleted `|| action.Touches(EntityKinds.Familiar, id)` again. `A_change_to_the_familiar_itself_reads_the_tree_again_with_its_mounts` waited and waited for `/web`… and `Single()` found no such node, because the tree never reloaded. 51 pass, 1 fail. **Killed.** A ring publishing *only* `EntityKinds.Familiar`, with no file touched, is precisely the knock that clause exists to answer~ #### 💡 The little ideas — also settled~ - `MoveAsync`'s single parse: `sourceRefused is null ? to : from` blames exactly what the old double parse blamed (I checked both refusal paths against the old expression), and a refused `from` now short-circuits before `to` is parsed at all. Same behaviour, one less parse~ - `Parse<T>` → `Parse`: all 11 call sites moved, build clean, no behaviour change. The unused parameter is gone~ #### ✅ What I liked~ - Your reasoning for skipping the "ring that only touched another file" variant was *correct*: a value-equal `FileView` makes `Sync` return before the guard, so that test could never have killed my mutant. Knowing which test **not** to write is the rarer skill, fufu~ - The doorbell test flips the mount through `SaveFamiliarSettings` — a real tool-group change — instead of hand-crafting a convenient ring. The mount mark assertion makes it about the *user-visible* consequence, not the dispatch~ Fufu~ both of my conditions are guarded now, and I have nothing left to sharpen my knife on. Merge it before I find something new to love~ ♡ --- *Automated review by Jibril · 2026-10-18* *CI: green for head SHA 97c3fedd · Mutation probes: 2 run, 0 survived (re-runs of the two 20fe572 survivors, now killed) · Local suite: skipped (CI green)*
bjoern merged commit 9302516990 into main 2026-10-18 10:53:29 +02:00
bjoern deleted branch feat/p1d-file-browser 2026-10-18 10:53:29 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Shikigami!9
No description provided.