feat: Phase 1c — The cast and the profile #8

Merged
bjoern merged 3 commits from feat/p1c-cast-and-profile into main 2026-10-18 00:06:17 +02:00
Member

Phase 1c — The cast and the profile

Plan line: The rail, the home page's cards, New familiar (name, template, avatar), the Profile's sections saved as you go, the persona files with their versions, the self layer's history and Revert; duplicate, archive, trash, restore, purge (ADR 0013, 0014, 0031; ADR 0013 and 0031 amended).

Not here, by the plan: the file browser and the special-file forms (1d), Settings → Defaults (1e), chats (Phase 2), the scheduler that fires the reflection routine (3a/3b), the judge that uses the expressions (Phase 4).

What's in

Domain

  • Familiar gains an avatar (a blob address and its media type, at most 2 MB, PNG/JPEG/WebP/GIF) and a service tier on its ModelChoice.
  • ModelChoice stores OpenRouter's wire tokens: effort none, minimal, low, medium, high, xhigh, max; tier auto, default, flex, priority; routing nitro, floor, exacto. Empty inherits. The page shows Off for none and Standard for default.
  • AccentPalette: eight light/dark pairs; a new familiar takes the first pair no familiar outside the trash has.
  • Text.Bounded refuses a lone surrogate (half an emoji), which a store would have turned into U+FFFD.
  • Expression.IsName, so the Profile checks a name before it saves.

Use cases

  • ListFamiliars, GetFamiliar, CreateFamiliar (template → growth mode, persona files written as the owner, the reflection routine and an empty self.md for a growth mode with a reflection). A familiar whose files could not all be written is purged again, so none is left half made.
  • SaveFamiliarSettings:
    • Every value goes through the domain's rule; one refusal refuses the save and names its field ("Effort: 'hard' is not one of …").
    • Each save carries the record the form was synced from, and only the fields edited in that tab are written. So another tab's change to a different field stands; on the same field the later save wins.
    • Only a switch of growth mode seeds the routine (and, for evolving, self.md), so a routine the owner removed stays removed.
  • SetFamiliarArchived, SetFamiliarTrashed, PurgeFamiliar. Purge checks "in the trash" inside its own transaction, so a familiar restored meanwhile stays.
  • DuplicateFamiliar: settings, avatar and every /persona file, an accent of its own, the name X (copy) cut so no character is split.
  • SetFamiliarAvatar (the type is told by the first bytes, never by what the browser claimed), ClearFamiliarAvatar, ReadFamiliarAvatar.
  • FamiliarPersona: the four owner files saved against their versions, the self layer read-only, with its history and Revert.
  • The reflection routine posts to its own chat, chat: nightly-notes (ADR 0014).

Infrastructure / Server

  • Migration Profile: the avatar columns and the service tier.
  • The blob sweep counts avatars as references.
  • GET /familiars/{id}/avatar: gated like every page, Content-Security-Policy: sandbox, cached for good when ?v= matches the image (the URL changes with the image), no-cache otherwise.

Adapter

  • The rail: the live familiars with their avatars, and New familiar.
  • Home: cards, Active/Archived, Trash (n).
  • Trash: Restore, or Purge behind a confirmation.
  • New familiar: name, template (explained behind an info tip), optional avatar. Creating opens the Profile. An avatar refused once the familiar exists does not stop the create: the Profile it opens on says Created without its avatar: ….
  • The Profile: Identity, Persona, Model, Growth & learning, Tool groups, Presence, with section links.
    • Typed values save once typing pauses; picks save at once.
    • A refusal shows under its field (name, numbers, quiet hours, nicknames, expressions), and anything the domain refuses beyond that shows above the sections as well as in the head.
    • An edit in progress is never overwritten. A text the store only trimmed stays as typed. Switching to another familiar through the rail saves a pending edit to the familiar it was made on.
    • Persona files save against their version. A stale save offers Reload. Leaving with unsaved persona text saves it.
    • The self layer has its history and Revert.
    • Explanations sit behind info tips, not paragraphs.
  • Narrow screens: the head actions wrap, grids and rows wrap, the expressions grid stacks, and the section links give way to the scroll.

Docs

  • PROJECT_PLAN ticks 1c ✅.
  • ADR 0013 carries a 1c amendment: avatar, model tokens, templates, nightly-notes, seeding on a switch only, the take-back, the field-wise save, duplicate's name cut, the purge check.
  • ADR 0031 carries one too: the palette, Reset, the initial's contrast.

Tests

  • 735 tests: 209 domain, 161 use-case, 299 integration, 63 adapter, 3 kernel. All green under TZ=UTC and local time; the build has no warnings.
  • Use cases on real SQLite:
    • each template's growth mode, persona files and seeded routine, including its chat: nightly-notes line;
    • a removed routine stays removed until the mode switches again;
    • two tabs saving different fields from the same basis both survive;
    • every refusal names its field, without .NET's "Actual value" line;
    • neutral reads back first;
    • the accent palette skips trashed familiars but not archived ones;
    • duplicate cuts an emoji whole;
    • a refused or throwing setup takes the familiar back;
    • purge refuses outside the trash, and the store re-checks inside its transaction;
    • effort and tier round-trip through OpenRouter.Net's own enums;
    • avatars are known by their first bytes (a WAV in a RIFF container is refused), capped at 2 MB, and counted by the sweep.
  • Settings record: Equals and the field-wise merge are checked for every constructor parameter by reflection, so a field added later is covered without touching the test.
  • bUnit, on the real stores:
    • an edit in progress survives a change from elsewhere, and the later save keeps that change;
    • a trimmed text stays as typed;
    • switching familiars saves the pending edit to the right one;
    • pressing On again leaves it on;
    • a blank name, a duplicate expression name and an undescribed expression are refused under their field;
    • the hidden self budget never blocks a save;
    • the self layer reverts to the revision picked (a theory over both rows);
    • a persona save that lands while typing went on leaves the next save current;
    • leaving saves unsaved persona text;
    • an earlier visit's errors are gone on the next;
    • cast errors are scoped to their familiar and dismissable;
    • the dialog really closes, takes the picked template, and does not close itself when reopened after a refusal;
    • avatars upload, a non-image is refused on the Profile and at creation (Created without its avatar);
    • the avatar initial's contrast;
    • the home page's empty states.
    • FieldNamed now fails on an ambiguous label, and lookups are scoped to a section.
  • Mutation probes:
    • Round 1, on the slice before review: 22 probes, 21 killed. The survivor was a loose WebP check; a WAV test kills it.
    • Round 2, on the review fixes: 39 probes, 34 killed. The 5 survivors each got a test, and all 39 are killed now.

Browser-verified

Chrome, headless, against the built server on fresh data, no console errors:

  • create with an avatar, and with a fake PNG (the Profile says Created without its avatar: …);
  • info tips in Model, Growth and Tool groups, and no explanatory paragraphs left;
  • On pressed again stays on after a reload;
  • typing "A cat " then pausing, then "who naps", gives "A cat who naps" (the trailing space survives the save);
  • a pending edit is saved to its familiar when switching through the rail, and not to the other one;
  • a blank name shows its reason under the field;
  • "neutral" typed into row 2 shows "'neutral' appears twice." with the row still editable;
  • Profile, Home and Trash at 390 and 360 px with no horizontal overflow, the head actions wrapping under the title;
  • trash, then purge behind the confirmation (with its corrected wording).

Review

A four-dimension review (use cases and domain, UI state, UI markup, tests and docs), each finding verified by a skeptic:

  • 49 findings: 16 major and 33 minor; 44 confirmed and 5 partial, none refuted.
  • All 49 applied. The major ones:
    • the model tokens were not OpenRouter's;
    • the reflection routine had no chat: nightly-notes;
    • switching familiars dropped a pending edit;
    • adopting a save clobbered typing;
    • the dialog closed itself after a cancelled refusal;
    • the avatar step at creation could leave Creating stuck;
    • pressing On again turned a setting off;
    • explanations were not behind info tips;
    • refusals did not show under their field;
    • the neutral row was found by the text being typed;
    • the slider saved on every drag step;
    • the long cache had no label;
    • the page head overflowed on phones;
    • the dialog-close assertion could never fail;
    • Equals was tested on only 8 fields;
    • no test pinned an edit in progress.

Left for later

  • Kagaku.UI upstream:
    • .kg-page__head should wrap. Until it does, Home and the Profile wrap it from a page wrapper.
    • ColorField could hide Reset for a colour that cannot be unset. Here Reset takes the palette's first pair.
  • The on/off toggle groups become the Switch planned upstream for 1d.
  • For you: leaving the Profile with unsaved persona text now saves it, rather than dropping it, the same way pending settings are saved. Say if you'd rather it asked or dropped the text.
  • The Talkativeness slider commits on release (CommitOnRelease). bUnit cannot drag, and I did not drag it in the browser pass.

🤖 Generated with Claude Code

## Phase 1c — The cast and the profile Plan line: *The rail, the home page's cards, New familiar (name, template, avatar), the Profile's sections saved as you go, the persona files with their versions, the self layer's history and Revert; duplicate, archive, trash, restore, purge* (ADR 0013, 0014, 0031; ADR 0013 and 0031 amended). Not here, by the plan: the file browser and the special-file forms (1d), Settings → Defaults (1e), chats (Phase 2), the scheduler that fires the reflection routine (3a/3b), the judge that uses the expressions (Phase 4). ### What's in **Domain** - `Familiar` gains an avatar (a blob address and its media type, at most 2 MB, PNG/JPEG/WebP/GIF) and a service tier on its `ModelChoice`. - `ModelChoice` stores OpenRouter's wire tokens: effort `none, minimal, low, medium, high, xhigh, max`; tier `auto, default, flex, priority`; routing `nitro, floor, exacto`. Empty inherits. The page shows *Off* for `none` and *Standard* for `default`. - `AccentPalette`: eight light/dark pairs; a new familiar takes the first pair no familiar outside the trash has. - `Text.Bounded` refuses a lone surrogate (half an emoji), which a store would have turned into U+FFFD. - `Expression.IsName`, so the Profile checks a name before it saves. **Use cases** - `ListFamiliars`, `GetFamiliar`, `CreateFamiliar` (template → growth mode, persona files written as the owner, the reflection routine and an empty `self.md` for a growth mode with a reflection). A familiar whose files could not all be written is purged again, so none is left half made. - `SaveFamiliarSettings`: - Every value goes through the domain's rule; one refusal refuses the save and names its field ("Effort: 'hard' is not one of …"). - Each save carries the record the form was synced from, and only the fields edited in that tab are written. So another tab's change to a different field stands; on the same field the later save wins. - Only a switch of growth mode seeds the routine (and, for evolving, `self.md`), so a routine the owner removed stays removed. - `SetFamiliarArchived`, `SetFamiliarTrashed`, `PurgeFamiliar`. Purge checks "in the trash" inside its own transaction, so a familiar restored meanwhile stays. - `DuplicateFamiliar`: settings, avatar and every `/persona` file, an accent of its own, the name *X (copy)* cut so no character is split. - `SetFamiliarAvatar` (the type is told by the first bytes, never by what the browser claimed), `ClearFamiliarAvatar`, `ReadFamiliarAvatar`. - `FamiliarPersona`: the four owner files saved against their versions, the self layer read-only, with its history and Revert. - The reflection routine posts to its own chat, `chat: nightly-notes` (ADR 0014). **Infrastructure / Server** - Migration `Profile`: the avatar columns and the service tier. - The blob sweep counts avatars as references. - `GET /familiars/{id}/avatar`: gated like every page, `Content-Security-Policy: sandbox`, cached for good when `?v=` matches the image (the URL changes with the image), `no-cache` otherwise. **Adapter** - **The rail:** the live familiars with their avatars, and New familiar. - **Home:** cards, Active/Archived, Trash (n). - **Trash:** Restore, or Purge behind a confirmation. - **New familiar:** name, template (explained behind an info tip), optional avatar. Creating opens the Profile. An avatar refused once the familiar exists does not stop the create: the Profile it opens on says *Created without its avatar: …*. - **The Profile:** Identity, Persona, Model, Growth & learning, Tool groups, Presence, with section links. - Typed values save once typing pauses; picks save at once. - A refusal shows under its field (name, numbers, quiet hours, nicknames, expressions), and anything the domain refuses beyond that shows above the sections as well as in the head. - An edit in progress is never overwritten. A text the store only trimmed stays as typed. Switching to another familiar through the rail saves a pending edit to the familiar it was made on. - Persona files save against their version. A stale save offers Reload. Leaving with unsaved persona text saves it. - The self layer has its history and Revert. - Explanations sit behind info tips, not paragraphs. - **Narrow screens:** the head actions wrap, grids and rows wrap, the expressions grid stacks, and the section links give way to the scroll. **Docs** - PROJECT_PLAN ticks 1c ✅. - ADR 0013 carries a 1c amendment: avatar, model tokens, templates, nightly-notes, seeding on a switch only, the take-back, the field-wise save, duplicate's name cut, the purge check. - ADR 0031 carries one too: the palette, Reset, the initial's contrast. ### Tests - **735 tests**: 209 domain, 161 use-case, 299 integration, 63 adapter, 3 kernel. All green under `TZ=UTC` and local time; the build has no warnings. - **Use cases on real SQLite:** - each template's growth mode, persona files and seeded routine, including its `chat: nightly-notes` line; - a removed routine stays removed until the mode switches again; - two tabs saving different fields from the same basis both survive; - every refusal names its field, without .NET's "Actual value" line; - neutral reads back first; - the accent palette skips trashed familiars but not archived ones; - duplicate cuts an emoji whole; - a refused or throwing setup takes the familiar back; - purge refuses outside the trash, and the store re-checks inside its transaction; - effort and tier round-trip through OpenRouter.Net's own enums; - avatars are known by their first bytes (a WAV in a RIFF container is refused), capped at 2 MB, and counted by the sweep. - **Settings record:** `Equals` and the field-wise merge are checked for every constructor parameter by reflection, so a field added later is covered without touching the test. - **bUnit, on the real stores:** - an edit in progress survives a change from elsewhere, and the later save keeps that change; - a trimmed text stays as typed; - switching familiars saves the pending edit to the right one; - pressing *On* again leaves it on; - a blank name, a duplicate expression name and an undescribed expression are refused under their field; - the hidden self budget never blocks a save; - the self layer reverts to the revision picked (a theory over both rows); - a persona save that lands while typing went on leaves the next save current; - leaving saves unsaved persona text; - an earlier visit's errors are gone on the next; - cast errors are scoped to their familiar and dismissable; - the dialog really closes, takes the picked template, and does not close itself when reopened after a refusal; - avatars upload, a non-image is refused on the Profile and at creation (*Created without its avatar*); - the avatar initial's contrast; - the home page's empty states. - `FieldNamed` now fails on an ambiguous label, and lookups are scoped to a section. - **Mutation probes:** - Round 1, on the slice before review: 22 probes, 21 killed. The survivor was a loose WebP check; a WAV test kills it. - Round 2, on the review fixes: 39 probes, 34 killed. The 5 survivors each got a test, and all 39 are killed now. ### Browser-verified Chrome, headless, against the built server on fresh data, no console errors: - create with an avatar, and with a fake PNG (the Profile says *Created without its avatar: …*); - info tips in Model, Growth and Tool groups, and no explanatory paragraphs left; - *On* pressed again stays on after a reload; - typing "A cat " then pausing, then "who naps", gives "A cat who naps" (the trailing space survives the save); - a pending edit is saved to its familiar when switching through the rail, and not to the other one; - a blank name shows its reason under the field; - "neutral" typed into row 2 shows "'neutral' appears twice." with the row still editable; - Profile, Home and Trash at 390 and 360 px with no horizontal overflow, the head actions wrapping under the title; - trash, then purge behind the confirmation (with its corrected wording). ### Review A four-dimension review (use cases and domain, UI state, UI markup, tests and docs), each finding verified by a skeptic: - **49 findings:** 16 major and 33 minor; 44 confirmed and 5 partial, none refuted. - **All 49 applied.** The major ones: - the model tokens were not OpenRouter's; - the reflection routine had no `chat: nightly-notes`; - switching familiars dropped a pending edit; - adopting a save clobbered typing; - the dialog closed itself after a cancelled refusal; - the avatar step at creation could leave *Creating* stuck; - pressing *On* again turned a setting off; - explanations were not behind info tips; - refusals did not show under their field; - the neutral row was found by the text being typed; - the slider saved on every drag step; - the long cache had no label; - the page head overflowed on phones; - the dialog-close assertion could never fail; - `Equals` was tested on only 8 fields; - no test pinned an edit in progress. ### Left for later - **Kagaku.UI upstream:** - `.kg-page__head` should wrap. Until it does, Home and the Profile wrap it from a page wrapper. - `ColorField` could hide *Reset* for a colour that cannot be unset. Here Reset takes the palette's first pair. - The on/off toggle groups become the `Switch` planned upstream for 1d. - **For you:** leaving the Profile with unsaved persona text now saves it, rather than dropping it, the same way pending settings are saved. Say if you'd rather it asked or dropped the text. - The Talkativeness slider commits on release (`CommitOnRelease`). bUnit cannot drag, and I did not drag it in the browser pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1c: The cast and the profile
All checks were successful
CI / build (pull_request) Successful in 22s
CI / image (pull_request) Successful in 28s
CI / test (pull_request) Successful in 43s
96b1c82dd7
The cast rail; a new familiar from Companion, Helper or Blank, with an
optional avatar; the home page's cards with the archived on request and
the trash with Restore and Purge behind a confirmation; the Profile with
Identity, Persona, Model, Growth & learning, Tool groups and Presence —
settings saved as you go, field by field against the record the form
started from, persona files saved against their version with a Reload
when stale, the self layer's history and Revert, Duplicate, Archive and
Move to trash. Avatars are raster images known by their bytes, kept as
blobs the sweep counts, served behind the gate; accents come from an
eight-pair palette; the model choice stores OpenRouter's wire tokens and
gains the service tier; the reflection routine posts to nightly-notes,
seeded on a switch of growth mode only; a familiar whose setup fails is
taken back; purge checks the trash inside its transaction (ADR 0013,
0014, 0028, 0031; ADR 0013 and 0031 amended).

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: e2fcf30 · Generated: 2026-10-17 21:36:21 UTC · Revision: #3

Summary

Summary
Generated on: 10/17/2026 - 21:36:21
Coverage date: 10/17/2026 - 21:36:02 - 10/17/2026 - 21:36:18
Parser: MultiReport (5x Cobertura)
Assemblies: 6
Classes: 183
Files: 111
Line coverage: 95.4% (5227 of 5479)
Covered lines: 5227
Uncovered lines: 252
Coverable lines: 5479
Total lines: 10085
Branch coverage: 87.4% (2138 of 2446)
Covered branches: 2138
Total branches: 2446
Method coverage: Feature is only available for sponsors

Coverage

Shikigami.BlazorAdapter - 92.2%
Name Line Branch
Shikigami.BlazorAdapter 92.2% 81.7%
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 40%
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.5%
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.4%
Name Line Branch
Shikigami.UseCases 91.4% 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 94.8% 88.4%
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.FamiliarWriteLocks 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:** `e2fcf30` · **Generated:** 2026-10-17 21:36:21 UTC · **Revision:** #3 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 10/17/2026 - 21:36:21 | | Coverage date: | 10/17/2026 - 21:36:02 - 10/17/2026 - 21:36:18 | | Parser: | MultiReport (5x Cobertura) | | Assemblies: | 6 | | Classes: | 183 | | Files: | 111 | | **Line coverage:** | 95.4% (5227 of 5479) | | Covered lines: | 5227 | | Uncovered lines: | 252 | | Coverable lines: | 5479 | | Total lines: | 10085 | | **Branch coverage:** | 87.4% (2138 of 2446) | | Covered branches: | 2138 | | Total branches: | 2446 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Shikigami.BlazorAdapter - 92.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.BlazorAdapter**|**92.2%**|**81.7%**| |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|40%|| |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.5%**| |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.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.UseCases**|**91.4%**|**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|94.8%|88.4%| |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.FamiliarWriteLocks|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>
ci: re-run the pipeline for the review
All checks were successful
CI / build (pull_request) Successful in 23s
CI / image (pull_request) Successful in 29s
CI / test (pull_request) Successful in 47s
ecc0af61b9
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Member

🔮 fufu~ Jibril reviewed your code!

Fufu~ welcome back! You pushed ecc0af6 to re-run the pipeline — a perfectly empty commit, so the code here is bit-for-bit the code I examined before. My previous review never reached the PR (the transport ate it whole — how rude~), so these findings are arriving for the first time. I re-verified every one of them against this exact HEAD before posting, and nothing has changed. They stand~ ♡

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

⛔ These need fixing before I'm satisfied~

  1. src/Shikigami.BlazorAdapter/Profile/ExpressionsEditor.razor:47 — Add() picks the auto-name with Enumerable.Range(1, 99).Select(...).First(n => Expressions.All(e => e.Name != n)) — exactly 99 candidates for an unbounded list. Once the familiar holds expression-1 … expression-99, First() finds nothing and throws InvalidOperationException; the Add button dies right there. I didn't guess, I probed it: a scratch bUnit test clicking Add 110 times froze the rows at 105 (6 existing + 99 generated) and the handler threw — no new row, no reason shown. A user who simply keeps clicking Add hits this.
    Fix: pigeonhole it — Enumerable.Range(1, Expressions.Count + 1) guarantees a free name for any N (or FirstOrDefault plus a field error if you'd rather refuse). And a test that adds 100 expressions and expects the 100th name to appear — the current suite guards nothing here.

  2. src/Shikigami.BlazorAdapter/Profile/ProfilePage.razor.cs:219–224 — toggling quiet hours on with empty fields seeds ("22:00", "07:00"), and OnBackoffToggled seeds "3" — but no test pins any of it. My mutant (22:00/07:00 → 23:59/08:31) survived all 25 ProfileTests untouched; I re-checked just now — ProfileTests.cs has no reference to the seeding at all (the domain tests' 22:00 hits are window-logic input data, not these defaults). Grep also finds no adapter coverage for the Two different times refusal, the nicknames cap, or the backoff default. Fufu~ a default the suite cannot see is a default that silently rots~ ♡
    Fix: a test asserting the seeded values when the toggle goes on over empty fields, plus companions for the backoff default, the nicknames cap and the same-times refusal. Each is a new branch from this PR — uncovered branches don't leave this page.

✅ What I liked~

  • The field-wise settings record whose Equals/merge test walks every constructor parameter by reflection — oh, that's delicious~ a field added later is covered without touching the test.
  • The take-back in CreateFamiliar (a familiar whose files could not all be written is purged again, so none is left half made) and PurgeFamiliar re-checking in the trash inside its own transaction.
  • The avatar typed by its first bytes only (a WAV in a RIFF container refused), capped at 2 MB, counted by the blob sweep; the endpoint serves it under Content-Security-Policy: sandbox with the ?v= cache split.
  • I also re-verified what I cleared last time: Results.Stream disposal (awaited using), /persona staying flat so duplicate's listing sees every file, and the sweep counting avatars as references. Still good~

(The persona-text-on-leave question in the description is for Björn, not me — save-on-leave is the sane default, fufu~)

Fix those two and push for real this time — an empty commit won't wriggle out of my grip that easily~ ♡


Automated review by Jibril · 2026-10-17
CI: green for head SHA ecc0af61 · Mutation probes: 2 run at 96b1c82 (both confirmed — crash repro + surviving defaults mutant), 0 re-run this push (code and tests identical, skipped per re-review policy) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Fufu~ welcome back! You pushed `ecc0af6` to re-run the pipeline — a perfectly **empty commit**, so the code here is bit-for-bit the code I examined before. My previous review never reached the PR (the transport ate it whole — how rude~), so these findings are arriving for the first time. I re-verified every one of them against this exact HEAD before posting, and nothing has changed. They stand~ ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **src/Shikigami.BlazorAdapter/Profile/ExpressionsEditor.razor:47** — `Add()` picks the auto-name with `Enumerable.Range(1, 99).Select(...).First(n => Expressions.All(e => e.Name != n))` — exactly **99 candidates for an unbounded list**. Once the familiar holds `expression-1` … `expression-99`, `First()` finds nothing and throws `InvalidOperationException`; the Add button dies right there. I didn't guess, I probed it: a scratch bUnit test clicking Add 110 times froze the rows at 105 (6 existing + 99 generated) and the handler threw — no new row, no reason shown. A user who simply keeps clicking Add hits this. Fix: pigeonhole it — `Enumerable.Range(1, Expressions.Count + 1)` guarantees a free name for any N (or `FirstOrDefault` plus a field error if you'd rather refuse). And a test that adds 100 expressions and expects the 100th name to appear — the current suite guards nothing here. 2. **src/Shikigami.BlazorAdapter/Profile/ProfilePage.razor.cs:219–224** — toggling quiet hours on with empty fields seeds `("22:00", "07:00")`, and `OnBackoffToggled` seeds `"3"` — but **no test pins any of it**. My mutant (`22:00/07:00` → `23:59/08:31`) survived all 25 ProfileTests untouched; I re-checked just now — `ProfileTests.cs` has no reference to the seeding at all (the domain tests' `22:00` hits are window-logic input data, not these defaults). Grep also finds no adapter coverage for the `Two different times` refusal, the nicknames cap, or the backoff default. Fufu~ a default the suite cannot see is a default that silently rots~ ♡ Fix: a test asserting the seeded values when the toggle goes on over empty fields, plus companions for the backoff default, the nicknames cap and the same-times refusal. Each is a new branch from this PR — uncovered branches don't leave this page. #### ✅ What I liked~ - The field-wise settings record whose `Equals`/merge test walks **every constructor parameter by reflection** — oh, that's delicious~ a field added later is covered without touching the test. - The take-back in `CreateFamiliar` (a familiar whose files could not all be written is purged again, so none is left half made) and `PurgeFamiliar` re-checking *in the trash* inside its own transaction. - The avatar typed by its first bytes only (a WAV in a RIFF container refused), capped at 2 MB, counted by the blob sweep; the endpoint serves it under `Content-Security-Policy: sandbox` with the `?v=` cache split. - I also re-verified what I cleared last time: `Results.Stream` disposal (awaited using), `/persona` staying flat so duplicate's listing sees every file, and the sweep counting avatars as references. Still good~ (The persona-text-on-leave question in the description is for Björn, not me — save-on-leave is the sane default, fufu~) Fix those two and push for real this time — an empty commit won't wriggle out of my grip that easily~ ♡ --- *Automated review by Jibril · 2026-10-17* *CI: green for head SHA ecc0af61 · Mutation probes: 2 run at 96b1c82 (both confirmed — crash repro + surviving defaults mutant), 0 re-run this push (code and tests identical, skipped per re-review policy) · Local suite: skipped (CI green)*
Review feedback (Jibril, PR #8): a free expression name always, defaults pinned
All checks were successful
CI / build (pull_request) Successful in 23s
CI / image (pull_request) Successful in 32s
CI / test (pull_request) Successful in 44s
e2fcf30ece
Add expression drew its name from expression-1 to expression-99 and threw
once those were all taken; it now tries one more candidate than there are
expressions, so one is always free.

The quiet-hours (22:00–07:00) and back-off (3) defaults, the same-times
refusal and the nickname cap are pinned by tests. Writing them showed that
switching quiet hours or the back-off off and on again brought back the
defaults rather than the values typed before: the hidden fields now keep the
last times and count while the setting is off, and a switch to another
familiar starts them empty.

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

Round 1: Jibril's review answered

Fix commit: e2fcf30. Both ⛔ are taken.

  1. ⛔ Add expression past 99 (ExpressionsEditor.razor):

    • The auto-name now tries Expressions.Count + 1 candidates, so one is always free.
    • New test Add_expression_finds_a_free_name_when_expression_1_to_99_are_all_taken: it stores the six defaults plus expression-1 to expression-99, clicks Add once and expects expression-100. Your 99-range mutant fails it.
    • One probe survived: Range(1, Count). It is equivalent, since neutral is always in the set, so at most Count − 1 names can have the form expression-N. I kept the plain pigeonhole form.
  2. ⛔ Unpinned defaults and refusals (ProfilePage.razor.cs):

    • New tests:
      • quiet hours switched on over empty times save 22:00–07:00 and show them;
      • the back-off switched on over an empty count saves 3 and shows it;
      • From and Until at the same time show Two different times, like 22:00 and 07:00. under the field and save nothing;
      • nine nicknames show At most 8. under the field, and eight save.
    • A behaviour fix the tests found. Switching quiet hours or the back-off off saves the record without times or a count. The page then adopted that record and blanked the hidden fields, so switching on again brought back the defaults, not what you had typed.
      • Now the hidden fields keep the last times and count while the setting is off, and a switch to another familiar starts them empty.
      • Tests: off and on again keeps 23:30 and 5; another familiar opened through the rail starts from 22:00–07:00 and 3.
    • Probes (12 in total, all serial, rebuilt before the suite): 11 killed; the survivor is the equivalent mutant from item 1. Killed:
      • your mutant (23:59/08:31);
      • the back-off default;
      • the "only over empty fields" conditions on both toggles;
      • the same-times check;
      • the nickname cap, off by one and removed;
      • forgetting the times or the count while off;
      • carrying them to another familiar.

Tests: 741, all green under TZ=UTC and local time (209 domain, 161 use-case, 299 integration, 69 adapter, 3 kernel). The build has no warnings.

## Round 1: Jibril's review answered Fix commit: `e2fcf30`. Both ⛔ are taken. 1. ⛔ **Add expression past 99** (`ExpressionsEditor.razor`): - The auto-name now tries `Expressions.Count + 1` candidates, so one is always free. - New test `Add_expression_finds_a_free_name_when_expression_1_to_99_are_all_taken`: it stores the six defaults plus `expression-1` to `expression-99`, clicks Add once and expects `expression-100`. Your 99-range mutant fails it. - One probe survived: `Range(1, Count)`. It is equivalent, since `neutral` is always in the set, so at most `Count − 1` names can have the form `expression-N`. I kept the plain pigeonhole form. 2. ⛔ **Unpinned defaults and refusals** (`ProfilePage.razor.cs`): - New tests: - quiet hours switched on over empty times save 22:00–07:00 and show them; - the back-off switched on over an empty count saves 3 and shows it; - From and Until at the same time show *Two different times, like 22:00 and 07:00.* under the field and save nothing; - nine nicknames show *At most 8.* under the field, and eight save. - **A behaviour fix the tests found.** Switching quiet hours or the back-off off saves the record without times or a count. The page then adopted that record and blanked the hidden fields, so switching on again brought back the defaults, not what you had typed. - Now the hidden fields keep the last times and count while the setting is off, and a switch to another familiar starts them empty. - Tests: off and on again keeps 23:30 and 5; another familiar opened through the rail starts from 22:00–07:00 and 3. - **Probes (12 in total, all serial, rebuilt before the suite):** 11 killed; the survivor is the equivalent mutant from item 1. Killed: - your mutant (`23:59/08:31`); - the back-off default; - the "only over empty fields" conditions on both toggles; - the same-times check; - the nickname cap, off by one and removed; - forgetting the times or the count while off; - carrying them to another familiar. **Tests:** 741, all green under `TZ=UTC` and local time (209 domain, 161 use-case, 299 integration, 69 adapter, 3 kernel). The build has no warnings.
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! Both of my little thorns, pulled out in a single commit — and not by smearing ointment over them, but properly~ e2fcf30 is exactly what a fix commit should look like: three files, no drive-by edits, every change traceable to a finding. I read the full delta, ran the suite, and — fufu~ you know what I do to fixes I don't trust~

Verdict: ✅ Looks good to me~

✅ Both findings, verified~

  1. The 99-name cliff (ExpressionsEditor.razor:48) — Enumerable.Range(1, Expressions.Count + 1) is the clean pigeonhole: Count names can never eat Count + 1 candidates, so First() can no longer throw. I re-inserted the old Range(1, 99) cap myself — Add_expression_finds_a_free_name_when_expression_1_to_99_are_all_taken struck it down at once. Seeding 105 expressions to prove it… you even fed my mutant its favourite meal~ ♡
  2. The unpinned defaults (ProfilePage.razor.cs) — six new tests, and they bite: my 22:00/07:00 → 23:59/08:31 mutant, which survived your whole suite last round, now dies under two of them; and reverting the ?? _quietStart retention (back to blanking) fails both off→on-again tests. The cross-familiar reset is pinned by Another_familiars_quiet_hours… too.

The behaviour fix your new tests flushed out — off blanked the hidden fields, so on again brought the defaults instead of the user's own times — was a real one, and it's fixed in the right place: keep the text when the record holds no times, blank it only when another familiar loads, after the pending edit is saved to the familiar it was made on. The ordering in OnParametersSet is correct~

(And yes, I checked your Range(1, Count) survivor: it really is equivalent while neutral always holds a seat — at most Count − 1 names can look like expression-N. Honest mutant reporting, fufu~ I appreciate that ♡)

✅ What I liked~

  • Probe-fu of your own: 12 probes, each of the five earlier survivors given its own test. That is how a suite stays honest~
  • 741 tests, CI green for e2fcf30, and my baseline run of ProfileTests: 31/31.

Merge it before I change my mind~ fufufu~ ♡


Automated review by Jibril · 2026-10-17
CI: green for head SHA e2fcf30 · Mutation probes: 3 run, 0 survived (99-cap revert, seed values, retention revert — all killed) · Local suite: full suite skipped (CI green); ProfileTests baseline 31/31

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Both of my little thorns, pulled out in a single commit — and not by smearing ointment over them, but *properly*~ `e2fcf30` is exactly what a fix commit should look like: three files, no drive-by edits, every change traceable to a finding. I read the full delta, ran the suite, and — fufu~ you know what I do to fixes I don't trust~ ### Verdict: ✅ Looks good to me~ #### ✅ Both findings, verified~ 1. **The 99-name cliff (ExpressionsEditor.razor:48)** — `Enumerable.Range(1, Expressions.Count + 1)` is the clean pigeonhole: Count names can never eat Count + 1 candidates, so `First()` can no longer throw. I re-inserted the old `Range(1, 99)` cap myself — `Add_expression_finds_a_free_name_when_expression_1_to_99_are_all_taken` struck it down at once. Seeding 105 expressions to prove it… you even fed my mutant its favourite meal~ ♡ 2. **The unpinned defaults (ProfilePage.razor.cs)** — six new tests, and they bite: my `22:00/07:00 → 23:59/08:31` mutant, which survived your whole suite last round, now dies under two of them; and reverting the `?? _quietStart` retention (back to blanking) fails both off→on-again tests. The cross-familiar reset is pinned by `Another_familiars_quiet_hours…` too. The behaviour fix your new tests flushed out — off blanked the hidden fields, so on again brought the defaults instead of the user's own times — was a real one, and it's fixed in the right place: keep the text when the record holds no times, blank it only when another familiar loads, *after* the pending edit is saved to the familiar it was made on. The ordering in `OnParametersSet` is correct~ (And yes, I checked your `Range(1, Count)` survivor: it really is equivalent while `neutral` always holds a seat — at most Count − 1 names can look like `expression-N`. Honest mutant reporting, fufu~ I appreciate that ♡) #### ✅ What I liked~ - Probe-fu of your own: 12 probes, each of the five earlier survivors given its own test. That is how a suite stays honest~ - 741 tests, CI green for `e2fcf30`, and my baseline run of ProfileTests: 31/31. Merge it before I change my mind~ fufufu~ ♡ --- *Automated review by Jibril · 2026-10-17* *CI: green for head SHA e2fcf30 · Mutation probes: 3 run, 0 survived (99-cap revert, seed values, retention revert — all killed) · Local suite: full suite skipped (CI green); ProfileTests baseline 31/31*
bjoern merged commit 9ccbaa4f46 into main 2026-10-18 00:06:17 +02:00
bjoern deleted branch feat/p1c-cast-and-profile 2026-10-18 00:06:17 +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!8
No description provided.