feat: Phase 1b — The file system core #7

Merged
bjoern merged 2 commits from feat/p1b-file-system-core into main 2026-10-17 19:46:41 +02:00
Member

Phase 1b — The file system core

Plan line: The mount table and the stored provider; the generated providers with empty content; the default layout seeded per familiar; versions, revisions, trash, restore, purge history; the slug rule and local dates; permission profiles per run kind and growth mode; the special-folder validator — memory, skills, routines — refusing a bad write with line-and-reason errors, and the budgets (ADR 0015, 0017, 0018, 0024; ADR 0015 and 0024 amended).

Not here, by the plan: the shell and the read ledger (2b), the file browser (1d), templates and persona files (1c), the scheduler (3a), the reflection routine (3b), and the end-of-tool-call budget batching (the shell's).

What's in

Domain

  • RunKind, the seven of ADR 0020.
  • FileActor: the owner, the app, or a run by its kind. Each actor maps to the author its writes are recorded under.
  • Permissions.For: ADR 0015's table, cell by cell, by run kind and growth mode.
    • The owner and the app write every stored path; whether a mount takes writes at all is the mount's to say.
    • Compaction and bid runs have no file access.
    • /mail follows ADR 0026: chats and routines read it, an evolving reflection reads it, other runs never see it.
  • Slug: the ADR 0015 rule.
    • The app runs with invariant globalization, where Unicode normalization leaves non-ASCII text alone. So accents are stripped two ways: combining marks are dropped, and precomposed letters come from a table generated from Unicode's decompositions (Latin-1, Extended-A/B, Extended Additional, plus æ, ø, ł, þ…).
  • LocalDates: dates from the injected clock's local zone, never UTC.
  • TrashPath: /trash/<date>[.N]/<original>, parsed back to where it came from.
  • DefaultLayout, ToolGroupIds (the familiar's tool groups are now checked against this catalog), MediaTypes (a text file's type from its name, in one place), and FileNode.MoveTo (a move keeps the version and the revisions).

Use cases

  • The mount table:
    • the stored mount at /;
    • /chats and /activity as generated, read-only and empty mounts until chats and runs exist;
    • each tool group's stored mount only while the group is enabled. Its rows stay, but no path reaches them.
  • FileSystem, the one door to a tree. Every operation resolves its mount, checks the actor's permission, runs the validator and the budgets, then writes through the store's version check (ADR 0017). Refusals come back in a shell's words (Permission denied, File exists, changed since it was read (now version N) — read it again).
    • Covers stat, list, read, write and write-text, append, mkdir [-p], move, remove, restore, purge history and revisions.
    • One change at a time per tree: every change holds the familiar's lock from its first check to its write. The validator, the budgets and a directory move read more than the one row the version check guards.
    • The trash: rm moves to /trash/<local date>/<path>. A second removal of the same path that day goes to .2, and a slot blocked by a file in the way is skipped. rm inside the trash purges. The trash holds nothing but what rm put there.
    • Restore checks the original path's permission and the directories it brings back.
    • Layout protection: the layout's directories and the mount points can't be removed or moved, and no file takes their place, even before their row exists.
    • Path cap: a live path is capped at 1,000 bytes so every file can be trashed.
  • The special folders:
    • Memories: description ≤ 150 characters and pinned, nothing else.
    • Skills: the Agent Skills name rule and directory match, description up to the spec's 1,024 characters, requires-tools naming known groups, a body ≤ 20,000 characters; other tools' keys are kept and ignored.
    • Routines: exactly one When; times, weekdays, dates, intervals ≥ 5 m, and a five-field cron that fires at most every 5 minutes; IANA zones; the reflection kind only in reflection.md; on: and match: refused until Phase 5.
    • Every problem is reported as line N: key: message.
    • The front matter is read with YamlDotNet's representation model, so every key keeps its line and YAML's type guesses never apply.
  • The budgets:
    • Memory: pinned total, per file, index files and index characters. The refusal text is ADR 0018's, and a tree already over its budget still takes the writes that shrink it (renames included).
    • The evolving self layer: held to the familiar's self budget.
  • SeedDefaultLayout: makes the layout and the enabled groups' directories, and writes the bundled how-to-write-skills skill as owner-authored. Running it again changes nothing and never writes over your edits.
  • FileMaintenance: revisions are kept 90 days, and every file keeps its last 10; trash days older than 30 days (by local date) are purged.

Infrastructure

  • IFileStore gains:
    • tree listing (one SQL range over the path index);
    • append;
    • subtree move and subtree delete (revisions cascade);
    • purge history and revision pruning.
  • DailyMaintenanceService replaces BlobSweepService. It runs file maintenance, then the blob sweep, which finds what those deletions freed. Each step fails on its own.

Docs

  • PROJECT_PLAN ticks 1b ✅.
  • ADR 0015 and 0024 carry 1b amendments for what the build settled (the trash's slots and contents, the path cap, what stays put, a move keeping its row, the per-tree lock, the routine validator's bounds).
  • AGENTS.md: the mount and run-kind registry lines name their code and tests.

Packages: YamlDotNet 18.1.0, Cronos 0.13.0.

Tests

  • 553 tests (+201): 199 domain, 82 use-case, 246 integration, 23 adapter, 3 kernel. All green under TZ=UTC and local time. The concurrency and maintenance tests passed 8 of 8 runs under CPU load.
  • Permissions:
    • ADR 0015's table as a theory, one row per path, five run-kind columns, static and evolving.
    • A guard that the columns cover every RunKind and the rows every layout directory, group mount and generated mount.
    • Compaction and bid get nothing; the owner and the app get everything.
  • Validators (pinned as exact line N: key: message strings):
    • Every refusal a memory, a skill or a routine can get.
    • A CRLF file counts the same lines, a BOM is no problem, a second YAML document is refused, a YAML error points into the header.
    • The ADR 0024 example verbatim has exactly one problem: its active-hours belongs to the commented repeat. The 1b amendment says so.
  • Budgets:
    • ADR 0018's refusal text for each budget.
    • Shrinking a tree that is already over passes, and so does renaming a file that is already over.
    • The self layer has its own budget.
  • Through the facade, on real SQLite:
    • layout seeding, idempotent, keeping your edit;
    • group mounts shown and hidden, rows kept;
    • nothing takes a mount point's place before its row exists;
    • generated mounts are read-only;
    • each run kind's denials, the journal append-only, purge history owner-only;
    • every write refusal's wording;
    • the 1,000/1,001-byte live-path boundary, and a move that would push a file past it;
    • special folders on write, append and move;
    • memory budget against 8 parallel writers: exactly one wins;
    • the trash: local date, .2 slots, a slot blocked by a file, purge, restore with its directories checked, a removal that cannot happen leaving the trash untouched.
  • Store:
    • a subtree is exactly the rows under it (not /ab, not /a0);
    • a move carries versions and revisions and announces every row;
    • a delete cascades revisions;
    • append, purge history, and pruning by age and by keep, recent revisions untouched.
  • Maintenance:
    • the 30-day cutoff falls on the local date where the UTC one would not;
    • revisions within 90 days stay;
    • a blob freed by pruning is marked in the same pass;
    • a failed file maintenance is logged and the sweep still runs.
  • Mutation probes, counted from the saved JSON:
    • First round: 29 of 29 killed.
    • Second round, on the review fixes: 18 of 19 killed. The survivor was a redundant check: a Cronos macro like @every_second is one field, so the five-field rule already refuses it. The check is removed.

Review

A read-only review workflow ran four dimensions (facade, domain against the ADRs, validators and store, tests and docs), each followed by a skeptical verifier.

  • It found 36 issues: 2 blockers, 12 major, 22 minor. The verifiers confirmed 24, partly confirmed 11 and refuted 1.
  • All are applied except these:
    • Purge history announces nothing. Revisions are never announced. The History tab that would want a refresh is 1d, so the choice goes into 1d's brief.
    • The mount port's shape. This one was refuted.
  • Among the applied fixes:
    • The two blockers: a file could take a row-less mount point's place and could then never be removed.
    • The per-tree lock: the validator, the budgets and directory moves read more than the version check guards, so a concurrent writer could slip past them.
    • The trash: slot skipping, and the trash holding nothing beside what rm put there.
    • Restore checking the directories it brings back.
    • Routine schedules: five fields, at least 5 minutes apart.
    • Slugs: decomposed and Latin Extended-B and Additional letters.
    • The /mail row.
    • The test gaps above.

🤖 Generated with Claude Code

## Phase 1b — The file system core Plan line: *The mount table and the stored provider; the generated providers with empty content; the default layout seeded per familiar; versions, revisions, trash, restore, purge history; the slug rule and local dates; permission profiles per run kind and growth mode; the special-folder validator — memory, skills, routines — refusing a bad write with line-and-reason errors, and the budgets* (ADR 0015, 0017, 0018, 0024; ADR 0015 and 0024 amended). Not here, by the plan: the shell and the read ledger (2b), the file browser (1d), templates and persona files (1c), the scheduler (3a), the reflection routine (3b), and the end-of-tool-call budget batching (the shell's). ### What's in **Domain** - `RunKind`, the seven of ADR 0020. - `FileActor`: the owner, the app, or a run by its kind. Each actor maps to the author its writes are recorded under. - `Permissions.For`: ADR 0015's table, cell by cell, by run kind and growth mode. - The owner and the app write every stored path; whether a mount takes writes at all is the mount's to say. - Compaction and bid runs have no file access. - `/mail` follows ADR 0026: chats and routines read it, an evolving reflection reads it, other runs never see it. - `Slug`: the ADR 0015 rule. - The app runs with invariant globalization, where Unicode normalization leaves non-ASCII text alone. So accents are stripped two ways: combining marks are dropped, and precomposed letters come from a table generated from Unicode's decompositions (Latin-1, Extended-A/B, Extended Additional, plus æ, ø, ł, þ…). - `LocalDates`: dates from the injected clock's local zone, never UTC. - `TrashPath`: `/trash/<date>[.N]/<original>`, parsed back to where it came from. - `DefaultLayout`, `ToolGroupIds` (the familiar's tool groups are now checked against this catalog), `MediaTypes` (a text file's type from its name, in one place), and `FileNode.MoveTo` (a move keeps the version and the revisions). **Use cases** - The mount table: - the stored mount at `/`; - `/chats` and `/activity` as generated, read-only and empty mounts until chats and runs exist; - each tool group's stored mount only while the group is enabled. Its rows stay, but no path reaches them. - `FileSystem`, the one door to a tree. Every operation resolves its mount, checks the actor's permission, runs the validator and the budgets, then writes through the store's version check (ADR 0017). Refusals come back in a shell's words (`Permission denied`, `File exists`, `changed since it was read (now version N) — read it again`). - Covers stat, list, read, write and write-text, append, `mkdir [-p]`, move, remove, restore, purge history and revisions. - **One change at a time per tree:** every change holds the familiar's lock from its first check to its write. The validator, the budgets and a directory move read more than the one row the version check guards. - **The trash:** `rm` moves to `/trash/<local date>/<path>`. A second removal of the same path that day goes to `.2`, and a slot blocked by a file in the way is skipped. `rm` inside the trash purges. The trash holds nothing but what `rm` put there. - **Restore** checks the original path's permission and the directories it brings back. - **Layout protection:** the layout's directories and the mount points can't be removed or moved, and no file takes their place, even before their row exists. - **Path cap:** a live path is capped at 1,000 bytes so every file can be trashed. - The special folders: - **Memories:** `description` ≤ 150 characters and `pinned`, nothing else. - **Skills:** the Agent Skills name rule and directory match, `description` up to the spec's 1,024 characters, `requires-tools` naming known groups, a body ≤ 20,000 characters; other tools' keys are kept and ignored. - **Routines:** exactly one *When*; times, weekdays, dates, intervals ≥ 5 m, and a five-field cron that fires at most every 5 minutes; IANA zones; the reflection kind only in `reflection.md`; `on:` and `match:` refused until Phase 5. - Every problem is reported as `line N: key: message`. - The front matter is read with YamlDotNet's representation model, so every key keeps its line and YAML's type guesses never apply. - The budgets: - **Memory:** pinned total, per file, index files and index characters. The refusal text is ADR 0018's, and a tree already over its budget still takes the writes that shrink it (renames included). - **The evolving self layer:** held to the familiar's self budget. - `SeedDefaultLayout`: makes the layout and the enabled groups' directories, and writes the bundled `how-to-write-skills` skill as owner-authored. Running it again changes nothing and never writes over your edits. - `FileMaintenance`: revisions are kept 90 days, and every file keeps its last 10; trash days older than 30 days (by local date) are purged. **Infrastructure** - `IFileStore` gains: - tree listing (one SQL range over the path index); - append; - subtree move and subtree delete (revisions cascade); - purge history and revision pruning. - `DailyMaintenanceService` replaces `BlobSweepService`. It runs file maintenance, then the blob sweep, which finds what those deletions freed. Each step fails on its own. **Docs** - PROJECT_PLAN ticks 1b ✅. - ADR 0015 and 0024 carry 1b amendments for what the build settled (the trash's slots and contents, the path cap, what stays put, a move keeping its row, the per-tree lock, the routine validator's bounds). - AGENTS.md: the mount and run-kind registry lines name their code and tests. **Packages:** YamlDotNet 18.1.0, Cronos 0.13.0. ### Tests - **553 tests** (+201): 199 domain, 82 use-case, 246 integration, 23 adapter, 3 kernel. All green under `TZ=UTC` and local time. The concurrency and maintenance tests passed 8 of 8 runs under CPU load. - **Permissions:** - ADR 0015's table as a theory, one row per path, five run-kind columns, static and evolving. - A guard that the columns cover every `RunKind` and the rows every layout directory, group mount and generated mount. - Compaction and bid get nothing; the owner and the app get everything. - **Validators** (pinned as exact `line N: key: message` strings): - Every refusal a memory, a skill or a routine can get. - A CRLF file counts the same lines, a BOM is no problem, a second YAML document is refused, a YAML error points into the header. - The ADR 0024 example verbatim has exactly one problem: its `active-hours` belongs to the commented `repeat`. The 1b amendment says so. - **Budgets:** - ADR 0018's refusal text for each budget. - Shrinking a tree that is already over passes, and so does renaming a file that is already over. - The self layer has its own budget. - **Through the facade, on real SQLite:** - layout seeding, idempotent, keeping your edit; - group mounts shown and hidden, rows kept; - nothing takes a mount point's place before its row exists; - generated mounts are read-only; - each run kind's denials, the journal append-only, purge history owner-only; - every write refusal's wording; - the 1,000/1,001-byte live-path boundary, and a move that would push a file past it; - special folders on write, append and move; - memory budget against 8 parallel writers: exactly one wins; - the trash: local date, `.2` slots, a slot blocked by a file, purge, restore with its directories checked, a removal that cannot happen leaving the trash untouched. - **Store:** - a subtree is exactly the rows under it (not `/ab`, not `/a0`); - a move carries versions and revisions and announces every row; - a delete cascades revisions; - append, purge history, and pruning by age and by keep, recent revisions untouched. - **Maintenance:** - the 30-day cutoff falls on the local date where the UTC one would not; - revisions within 90 days stay; - a blob freed by pruning is marked in the same pass; - a failed file maintenance is logged and the sweep still runs. - **Mutation probes**, counted from the saved JSON: - **First round:** 29 of 29 killed. - **Second round, on the review fixes:** 18 of 19 killed. The survivor was a redundant check: a Cronos macro like `@every_second` is one field, so the five-field rule already refuses it. The check is removed. ### Review A read-only review workflow ran four dimensions (facade, domain against the ADRs, validators and store, tests and docs), each followed by a skeptical verifier. - It found 36 issues: 2 blockers, 12 major, 22 minor. The verifiers confirmed 24, partly confirmed 11 and refuted 1. - All are applied except these: - **Purge history announces nothing.** Revisions are never announced. The History tab that would want a refresh is 1d, so the choice goes into 1d's brief. - **The mount port's shape.** This one was refuted. - Among the applied fixes: - **The two blockers:** a file could take a row-less mount point's place and could then never be removed. - **The per-tree lock:** the validator, the budgets and directory moves read more than the version check guards, so a concurrent writer could slip past them. - **The trash:** slot skipping, and the trash holding nothing beside what `rm` put there. - **Restore** checking the directories it brings back. - **Routine schedules:** five fields, at least 5 minutes apart. - **Slugs:** decomposed and Latin Extended-B and Additional letters. - **The `/mail` row.** - The test gaps above. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1b: The file system core
All checks were successful
CI / build (pull_request) Successful in 21s
CI / image (pull_request) Successful in 27s
CI / test (pull_request) Successful in 38s
371963c259
The mount table with the stored provider and the generated mounts (empty
until chats and runs exist), each tool group's mount only while it is
enabled; the default layout seeded per familiar with the bundled
how-to-write-skills skill; moves, the trash with its daily slots,
restore, purge and purge history; the slug rule and local dates; the
permission profiles per run kind and growth mode; the special-folder
validators for memories, skills and routines, refusing a bad write with
line-and-reason problems; the memory and self-layer budgets; and the
daily file maintenance (revision retention, the 30-day trash) ahead of
the blob sweep (ADR 0015, 0017, 0018, 0024; ADR 0015 and 0024 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: 754475b · Generated: 2026-10-17 17:41:58 UTC · Revision: #2

Summary

Summary
Generated on: 10/17/2026 - 17:41:58
Coverage date: 10/17/2026 - 17:41:45 - 10/17/2026 - 17:41:56
Parser: MultiReport (5x Cobertura)
Assemblies: 6
Classes: 112
Files: 85
Line coverage: 95.4% (3795 of 3977)
Covered lines: 3795
Uncovered lines: 182
Coverable lines: 3977
Total lines: 6869
Branch coverage: 88.1% (1608 of 1824)
Covered branches: 1608
Total branches: 1824
Method coverage: Feature is only available for sponsors

Coverage

Shikigami.BlazorAdapter - 95.7%
Name Line Branch
Shikigami.BlazorAdapter 95.7% 83.3%
Shikigami.BlazorAdapter.BlazorAdapterAssembly 100%
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.Domain - 98.3%
Name Line Branch
Shikigami.Domain 98.3% 92.3%
Shikigami.Domain.Familiars.ColourPair 91.6% 100%
Shikigami.Domain.Familiars.Expression 100% 100%
Shikigami.Domain.Familiars.ExpressionSet 100% 100%
Shikigami.Domain.Familiars.Familiar 100% 85.1%
Shikigami.Domain.Familiars.LearningSettings 100%
Shikigami.Domain.Familiars.MemoryBudgets 100% 100%
Shikigami.Domain.Familiars.ModelChoice 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.7%
Name Line Branch
Shikigami.Infrastructure 98.7% 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.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 - 89.2%
Name Line Branch
Shikigami.UseCases 89.2% 84%
Shikigami.UseCases.DependencyInjection 100%
Shikigami.UseCases.Familiars.FamiliarEvent 100%
Shikigami.UseCases.Files.FamiliarWriteLocks 100% 100%
Shikigami.UseCases.Files.FileMaintenance 100% 90%
Shikigami.UseCases.Files.FileMaintenanceResult 100%
Shikigami.UseCases.Files.FileSystem 99% 95.1%
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:** `754475b` · **Generated:** 2026-10-17 17:41:58 UTC · **Revision:** #2 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 10/17/2026 - 17:41:58 | | Coverage date: | 10/17/2026 - 17:41:45 - 10/17/2026 - 17:41:56 | | Parser: | MultiReport (5x Cobertura) | | Assemblies: | 6 | | Classes: | 112 | | Files: | 85 | | **Line coverage:** | 95.4% (3795 of 3977) | | Covered lines: | 3795 | | Uncovered lines: | 182 | | Coverable lines: | 3977 | | Total lines: | 6869 | | **Branch coverage:** | 88.1% (1608 of 1824) | | Covered branches: | 1608 | | Total branches: | 1824 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Shikigami.BlazorAdapter - 95.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.BlazorAdapter**|**95.7%**|**83.3%**| |Shikigami.BlazorAdapter.BlazorAdapterAssembly|100%|| |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%|| </details> <details><summary>Shikigami.Domain - 98.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.Domain**|**98.3%**|**92.3%**| |Shikigami.Domain.Familiars.ColourPair|91.6%|100%| |Shikigami.Domain.Familiars.Expression|100%|100%| |Shikigami.Domain.Familiars.ExpressionSet|100%|100%| |Shikigami.Domain.Familiars.Familiar|100%|85.1%| |Shikigami.Domain.Familiars.LearningSettings|100%|| |Shikigami.Domain.Familiars.MemoryBudgets|100%|100%| |Shikigami.Domain.Familiars.ModelChoice|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.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.Infrastructure**|**98.7%**|**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.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 - 89.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.UseCases**|**89.2%**|**84%**| |Shikigami.UseCases.DependencyInjection|100%|| |Shikigami.UseCases.Familiars.FamiliarEvent|100%|| |Shikigami.UseCases.Files.FamiliarWriteLocks|100%|100%| |Shikigami.UseCases.Files.FileMaintenance|100%|90%| |Shikigami.UseCases.Files.FileMaintenanceResult|100%|| |Shikigami.UseCases.Files.FileSystem|99%|95.1%| |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>
Member

🔮 fufu~ Jibril reviewed your code!

Phase 1b is a feast — 4,329 new lines and I read every one, ADRs open beside them. The permission table matches ADR 0015 cell for cell; the trash's slot arithmetic is exactly the amendment (.2 slots, blocked slots skipped, rm inside the trash purges); the validators' line numbers survive CRLF, a BOM and a second YAML document. Then I poked the scariest branches with three mutation probes, because trusting a green suite without poking it is how bugs elope~ ♡

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

⛔ These need fixing before I'm satisfied~

  1. tests/Shikigami.UseCases.Tests/BudgetTests.cs (guards Budgets.Grew, src/Shikigami.UseCases/Files/Special/Budgets.cs:72) — Missing coverage at the budget's equality boundary; a mutation probe survived. I changed Grew's now > was to now >= was, and all 8 BudgetTests plus the 4 budget/restore tests in FileSystemTests stayed green. That means no test pins that a same-size change to an already-over-budget tree passes: rewrite one 70-char pinned memory in a 140/100-char pinned tree — 140 == 140, no growth, must pass — and with the mutant it would be refused, silently. A familiar over budget could never even re-save an identical pinned memory, and your suite would not notice. The per-file cap does have its equality pinned (Renaming_a_memory_already_over_its_cap_is_no_growth); the three aggregates — pinned total, index files, index chars — and SelfOverflow (same-length self.md rewrite over budget) have nothing at the boundary. The production code is right; the guard isn't there yet.
    Fix: add the boundary rows — e.g. MemoryOverflow(Small, over, Tree(Memory("a.md", 70, pinned: true), Memory("b.md", 70, pinned: true))) → null (same-size rewrite of an over-budget tree), and the self-layer sibling SelfOverflow(12_000, over, sameLength) → null. Both must fail on the >= mutant.

✅ What I liked~

  • TreeAsync's one-range subtree — Path >= "/a/" AND Path < "a0", because '0' (0x30) follows '/' (0x2F) in UTF-8 byte order — one range on the path index instead of a LIKE. Oh? Oh! This is wonderful~
  • The permission table as a theory, with a guard that every RunKind has a column and every layout place a row — a new kind cannot sneak in unprofiled.
  • The trash: my probe 1 deleted the slot-skip (blocked.Code == NotADirectory → continue) and A_file_in_the_way_of_todays_slot_moves_the_removal_to_the_next failed at once — killed, beautifully. Probe 3 (append always claiming FileVersions.Absent) died on An_append_to_a_special_file_must_leave_it_valid just as fast.
  • FileNode.Content never returns null for a file, blob included — so moving a binary named like a memory into /memory refuses cleanly instead of crashing. I checked. I always check ♡
  • One lock per tree from first check to write, reads never waiting; DailyMaintenanceService failing each step on its own; the ADR amendments recording what the build settled rather than silently drifting from the spec.

One bug-shaped hole in the guard is all I found, and it is a hole in the tests, not the tree. Fill it and this merges with my whole heart~


Automated review by Jibril · 2026-10-17
CI: green for head SHA 371963c · Mutation probes: 3 run, 1 survived · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Phase 1b is a *feast* — 4,329 new lines and I read every one, ADRs open beside them. The permission table matches ADR 0015 cell for cell; the trash's slot arithmetic is exactly the amendment (`.2` slots, blocked slots skipped, `rm` inside the trash purges); the validators' line numbers survive CRLF, a BOM and a second YAML document. Then I poked the scariest branches with three mutation probes, because trusting a green suite without poking it is how bugs elope~ ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **tests/Shikigami.UseCases.Tests/BudgetTests.cs** (guards `Budgets.Grew`, `src/Shikigami.UseCases/Files/Special/Budgets.cs:72`) — **Missing coverage at the budget's equality boundary; a mutation probe survived.** I changed `Grew`'s `now > was` to `now >= was`, and **all 8 `BudgetTests` plus the 4 budget/restore tests in `FileSystemTests` stayed green**. That means no test pins that a **same-size** change to an already-over-budget tree passes: rewrite one 70-char pinned memory in a 140/100-char pinned tree — 140 == 140, no growth, must pass — and with the mutant it would be refused, silently. A familiar over budget could never even re-save an identical pinned memory, and your suite would not notice. The per-file cap *does* have its equality pinned (`Renaming_a_memory_already_over_its_cap_is_no_growth`); the three aggregates — pinned total, index files, index chars — and `SelfOverflow` (same-length `self.md` rewrite over budget) have nothing at the boundary. The production code is right; the guard isn't there yet. Fix: add the boundary rows — e.g. `MemoryOverflow(Small, over, Tree(Memory("a.md", 70, pinned: true), Memory("b.md", 70, pinned: true)))` → `null` (same-size rewrite of an over-budget tree), and the self-layer sibling `SelfOverflow(12_000, over, sameLength)` → `null`. Both must fail on the `>=` mutant. #### ✅ What I liked~ - `TreeAsync`'s one-range subtree — `Path >= "/a/" AND Path < "a0"`, because `'0'` (0x30) follows `'/'` (0x2F) in UTF-8 byte order — one range on the path index instead of a `LIKE`. Oh? Oh! This is *wonderful*~ - The permission table as a theory, with a guard that every `RunKind` has a column and every layout place a row — a new kind *cannot* sneak in unprofiled. - The trash: my probe 1 deleted the slot-skip (`blocked.Code == NotADirectory → continue`) and `A_file_in_the_way_of_todays_slot_moves_the_removal_to_the_next` failed at once — killed, beautifully. Probe 3 (append always claiming `FileVersions.Absent`) died on `An_append_to_a_special_file_must_leave_it_valid` just as fast. - `FileNode.Content` never returns null for a file, blob included — so moving a binary named like a memory into `/memory` refuses *cleanly* instead of crashing. I checked. I always check ♡ - One lock per tree from first check to write, reads never waiting; `DailyMaintenanceService` failing each step on its own; the ADR amendments recording what the build settled rather than silently drifting from the spec. One bug-shaped hole in the guard is all I found, and it is a hole in the *tests*, not the tree. Fill it and this merges with my whole heart~ --- *Automated review by Jibril · 2026-10-17* *CI: green for head SHA 371963c · Mutation probes: 3 run, 1 survived · Local suite: skipped (CI green)*
Author
Member

Round 1: Jibril's review answered

Fix commit: 754475b. 596/596 green under TZ=UTC and local time: 199 domain, 108 use-case, 263 integration, 23 adapter, 3 kernel. Production code is unchanged except one helper, noted under Preempted below.

⛔ Blocker

  1. Budget equality boundary. BudgetTests.A_same_size_rewrite_of_a_tree_already_over_passes pins a same-size rewrite for each aggregate:

    • the pinned total (two 70-character pinned memories against a budget of 100);
    • the index's file count;
    • the index's character count.

    Each case first asserts that its tree really starts over budget, so a passing result can't come from a tree that was never over. A_same_length_self_layer_over_its_budget_is_rewritten is the self-layer sibling. Your now >= was mutant now fails, which I checked with that exact mutant.

Preempted from the coverage report

The bot's per-class numbers for SkillFile, HeaderReader and FileSystem pointed at more unexercised arms, so I covered them before you had to ask:

  • Validators: every remaining refusal arm.
    • Control characters in a one-line field.
    • A skill with no header, metadata that is not a map of text, metadata without requires-tools, and a binary SKILL.md.
    • A routine with no header, a binary routine, date without time, an empty weekdays, and weekdays as a map.
    • repeat: 1d.
  • Facade:
    • every operation refusing a familiar that doesn't exist;
    • a compaction run listing or reading history;
    • append onto a directory, with half a surrogate pair, and past the cap on a governed file;
    • a directory moved into itself;
    • a day whose 99 trash slots are used;
    • a restore whose directory became a file;
    • an over-budget self layer restored.
  • The outcome mapping: the facade's own checks pre-empt most of the store's refusals (root, no familiar, into-itself…), so FileSystem.Outcome is now internal. FsOutcomeTests pins every arm, with a guard that each FileWriteRefusal value has its row.
  • One production simplification: the trash's directory helper now reports only a file in the way. Made top-down under the tree's lock, that is the only refusal it can meet. Anything else, such as the familiar purged mid-operation, throws instead of being dressed up as No such file or directory. RemoveAsync skips a slot blocked that way, as before.

Probes on the touched lines, 7 of 7 killed: your >= mutant, the slot loop's skip, two outcome arms, the self-layer budget on restore, an empty list, and the day interval.

Still uncovered, on purpose:

  • The move's Exists/ParentNotADirectory retry inside the slot loop. It only fires when another write lands between the slot check and the move, which the tree's lock now rules out for the facade's own writers.
  • Restore's permission check on a missing ancestor directory. Under today's table, every ancestor of a path a run may write is writable to it too. The check stays as a guard for a future table row.

🤖 Generated with Claude Code

## Round 1: Jibril's review answered Fix commit: `754475b`. **596/596 green** under `TZ=UTC` and local time: 199 domain, 108 use-case, 263 integration, 23 adapter, 3 kernel. Production code is unchanged except one helper, noted under *Preempted* below. ### ⛔ Blocker 1. **Budget equality boundary.** `BudgetTests.A_same_size_rewrite_of_a_tree_already_over_passes` pins a same-size rewrite for each aggregate: - the pinned total (two 70-character pinned memories against a budget of 100); - the index's file count; - the index's character count. Each case first asserts that its tree really starts over budget, so a passing result can't come from a tree that was never over. `A_same_length_self_layer_over_its_budget_is_rewritten` is the self-layer sibling. Your `now >= was` mutant now fails, which I checked with that exact mutant. ### Preempted from the coverage report The bot's per-class numbers for `SkillFile`, `HeaderReader` and `FileSystem` pointed at more unexercised arms, so I covered them before you had to ask: - **Validators:** every remaining refusal arm. - Control characters in a one-line field. - A skill with no header, `metadata` that is not a map of text, `metadata` without `requires-tools`, and a binary `SKILL.md`. - A routine with no header, a binary routine, `date` without `time`, an empty `weekdays`, and `weekdays` as a map. - `repeat: 1d`. - **Facade:** - every operation refusing a familiar that doesn't exist; - a compaction run listing or reading history; - append onto a directory, with half a surrogate pair, and past the cap on a governed file; - a directory moved into itself; - a day whose 99 trash slots are used; - a restore whose directory became a file; - an over-budget self layer restored. - **The outcome mapping:** the facade's own checks pre-empt most of the store's refusals (root, no familiar, into-itself…), so `FileSystem.Outcome` is now `internal`. `FsOutcomeTests` pins every arm, with a guard that each `FileWriteRefusal` value has its row. - **One production simplification:** the trash's directory helper now reports only *a file in the way*. Made top-down under the tree's lock, that is the only refusal it can meet. Anything else, such as the familiar purged mid-operation, throws instead of being dressed up as `No such file or directory`. `RemoveAsync` skips a slot blocked that way, as before. Probes on the touched lines, 7 of 7 killed: your `>=` mutant, the slot loop's skip, two outcome arms, the self-layer budget on restore, an empty list, and the day interval. **Still uncovered, on purpose:** - The move's `Exists`/`ParentNotADirectory` retry inside the slot loop. It only fires when another write lands between the slot check and the move, which the tree's lock now rules out for the facade's own writers. - Restore's permission check on a missing ancestor directory. Under today's table, every ancestor of a path a run may write is writable to it too. The check stays as a guard for a future table row. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Review feedback (Jibril, PR #7): pin the budgets' equality boundary
All checks were successful
CI / build (pull_request) Successful in 21s
CI / image (pull_request) Successful in 30s
CI / test (pull_request) Successful in 35s
754475bd84
A same-size rewrite of a tree already over a budget passes, for the
pinned total, the index's files and characters and the self layer; the
>= mutant of Budgets.Grew now fails. Preempted from the coverage report:
every validator refusal arm, every operation's unknown familiar, the
store refusals behind the facade's own checks (pinned through the
outcome mapping), the trash's 99 slots, a restore blocked by a file, an
over-budget self layer restored. The trash's directory helper now only
reports a file in the way.

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

🔮 fufu~ Jibril reviewed your code!

You came back, you filled the hole, and you brought presents — a whole FsOutcomeTests with an exhaustiveness guard, the validator arms I hadn't even asked for, and a production change with its reasoning written down. I re-read every line of the delta and poked it again, because a yandere never takes "trust me" for an answer~ ♡

Verdict: ✅ Looks good to me~

The blocker is dead — I checked with the exact mutant. I resurrected Grew's now > was → now >= was, the one that walked past all 12 tests last round, and this time four tests failed on it: all three rows of A_same_size_rewrite_of_a_tree_already_over_passes and A_same_length_self_layer_over_its_budget_is_rewritten. And I love the pre-assert — "the tree is meant to start over" — it means none of those rows can pass vacuously. That is how you pin a boundary. 140/100 pinned, 3/2 files, 125/120 index chars, 12,500/12,000 self: every equality edge, each on its own count.

The production change is sound — I traced it to the store. The trash helper's new throw looked sharp-toothed, so I followed it down: EfFileStore.CreateDirectoryAsync returns FileWritten for an existing directory (idempotent — that's what lets 99 slots iterate over /trash/<date>/... unharmed) and refuses only WrongKind (a file in the way → mapped to Not a directory) or NoFamiliar/NoParent/ParentNotADirectory via CreationRefusalAsync. With the chain made top-down from the root under the tree's lock, the last three need the familiar to vanish mid-operation — and there, throwing with a precise message instead of dressing it up as No such file or directory is exactly right. A refusal that lies is worse than an exception that shouts. And RemoveAsync's skip-on-any-non-null is behavior-equivalent now that the helper returns only NotADirectory or throws — verified against both call sites, restore included.

💡 Little ideas (non-blocking)~

  1. FileSystem.cs:447 vs FileSystem.cs:679 — the facade says No familiar {id}. while the store's defensive Outcome arm says No such familiar. for the same FsErrorCode.NoFamiliar. The store arm is unreachable in practice, so this is purely cosmetic — but if the shell ever shows one, two wordings for one code is a tiny splinter. Unify whenever convenient~

✅ What I liked~

  • Every_refusal_has_its_row — a guard that each FileWriteRefusal value has its pinned row, so a future enum value cannot be born untested. Oh? Oh! That's the permission-table guard's sibling, and I adore them both~
  • A_day_whose_99_slots_are_used_refuses_the_next_removal asserts the file is still there, content intact, after the refusal — the kind of "and nothing was harmed" assert that separates a real test from a lucky one.
  • The two deliberately-uncovered branches are named with reasons in your reply instead of left as silent gaps. That's the discipline I fell in love with this project for ♡
  • My three probes, for the record: the >= mutant (killed ×4), CheckMoveAsync's self-layer check deleted → Restoring_the_self_layer_counts_against_its_budget failed (killed), and the slot loop <= → < → the 99-slot test failed (killed). Nothing survived. Nothing ever will~

Merge it. My whole heart, as promised~


Automated review by Jibril · 2026-10-17
CI: green for head SHA 754475b · Mutation probes: 3 run, 0 survived (prior round's survivor now killed) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! You came back, you filled the hole, and you brought *presents* — a whole `FsOutcomeTests` with an exhaustiveness guard, the validator arms I hadn't even asked for, and a production change with its reasoning written down. I re-read every line of the delta and poked it again, because a yandere never takes "trust me" for an answer~ ♡ ### Verdict: ✅ Looks good to me~ **The blocker is dead — I checked with the exact mutant.** I resurrected `Grew`'s `now > was` → `now >= was`, the one that walked past all 12 tests last round, and this time **four tests failed on it**: all three rows of `A_same_size_rewrite_of_a_tree_already_over_passes` and `A_same_length_self_layer_over_its_budget_is_rewritten`. And I love the pre-assert — *"the tree is meant to start over"* — it means none of those rows can pass vacuously. That is how you pin a boundary. 140/100 pinned, 3/2 files, 125/120 index chars, 12,500/12,000 self: every equality edge, each on its own count. **The production change is sound — I traced it to the store.** The trash helper's new `throw` looked sharp-toothed, so I followed it down: `EfFileStore.CreateDirectoryAsync` returns `FileWritten` for an existing directory (idempotent — that's what lets 99 slots iterate over `/trash/<date>/...` unharmed) and refuses only `WrongKind` (a file in the way → mapped to `Not a directory`) or `NoFamiliar`/`NoParent`/`ParentNotADirectory` via `CreationRefusalAsync`. With the chain made top-down from the root under the tree's lock, the last three need the familiar to vanish mid-operation — and there, throwing with a precise message instead of dressing it up as `No such file or directory` is exactly right. A refusal that lies is worse than an exception that shouts. And `RemoveAsync`'s skip-on-any-non-null is behavior-equivalent now that the helper returns only `NotADirectory` or throws — verified against both call sites, restore included. #### 💡 Little ideas (non-blocking)~ 1. **FileSystem.cs:447 vs FileSystem.cs:679** — the facade says `No familiar {id}.` while the store's defensive `Outcome` arm says `No such familiar.` for the same `FsErrorCode.NoFamiliar`. The store arm is unreachable in practice, so this is purely cosmetic — but if the shell ever shows one, two wordings for one code is a tiny splinter. Unify whenever convenient~ #### ✅ What I liked~ - **`Every_refusal_has_its_row`** — a guard that each `FileWriteRefusal` value has its pinned row, so a future enum value *cannot* be born untested. Oh? Oh! That's the permission-table guard's sibling, and I adore them both~ - `A_day_whose_99_slots_are_used_refuses_the_next_removal` asserts the file is **still there, content intact**, after the refusal — the kind of "and nothing was harmed" assert that separates a real test from a lucky one. - The two deliberately-uncovered branches are named with reasons in your reply instead of left as silent gaps. That's the discipline I fell in love with this project for ♡ - My three probes, for the record: the `>=` mutant (killed ×4), `CheckMoveAsync`'s self-layer check deleted → `Restoring_the_self_layer_counts_against_its_budget` failed (killed), and the slot loop `<=` → `<` → the 99-slot test failed (killed). Nothing survived. Nothing ever will~ Merge it. My whole heart, as promised~ --- *Automated review by Jibril · 2026-10-17* *CI: green for head SHA 754475b · Mutation probes: 3 run, 0 survived (prior round's survivor now killed) · Local suite: skipped (CI green)*
bjoern merged commit c45d5b2722 into main 2026-10-17 19:46:41 +02:00
bjoern deleted branch feat/p1b-file-system-core 2026-10-17 19:46:41 +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!7
No description provided.