feat: Phase 1a — Domain and persistence #6

Merged
bjoern merged 2 commits from feat/p1a-domain-persistence into main 2026-10-17 18:04:08 +02:00
Member

Phase 1a — Domain and persistence

Plan line: Familiars and their settings; file rows, revisions and blobs with the sweep's grace period; the pragma interceptor and the WriteAsync path, tested with concurrent writers from the start (ADR 0005, 0013, 0015, 0017). No UI and no use cases beyond the stores: the mount table, shell, trash and permissions are 1b, the cast and profile 1c.

What lands

Domain (Shikigami.Domain, pure)

  • Familiar with every setting from ADR 0013:

    • name, up to 8 nicknames, description
    • accent ColourPair, ModelChoice (blank fields inherit)
    • growth mode and self budget, learning switches, memory budgets
    • tool groups (Core implied, never stored), talkativeness
    • ExpressionSet (the six VRM presets, each with a description, neutral always present)
    • outreach (quiet hours to the minute, unanswered back-off)
    • archive and trash.

    A mutator handed what the familiar already holds changes nothing, so the save writes and announces nothing. Every bounded text field is a single line.

  • FilePath: POSIX, /-rooted, canonical. NUL and lone surrogates are refused; a name is at most 255 UTF-8 bytes and a path at most 1024.

  • Files and contents:

    • FileNode: a file or a directory, with a version, a last author and either text or a blob.
    • FileRevision: one per write, with author, time and the tainted mark.
    • TextContent: at most 256 KB of valid UTF-8, and its media type is always chosen by the caller.
    • BlobContent with BlobSha.
    • FileAuthor: one of seven author kinds.

Ports (Shikigami.UseCases)

  • IFamiliarStore and IBlobStore.
  • IFileStore: get, list children, write against an expected version, create directory, revisions.
    • A write returns FileWritten (with no revision when nothing changed) or FileWriteRefused: Conflict, NoParent, ParentNotADirectory, WrongKind, NoFamiliar or Root.
  • EntityKinds.Familiar and EntityKinds.File are added. No feature effects consume them yet: the cast UI is 1c.

Persistence (Shikigami.Infrastructure)

  • ShikigamiDbContext:
    • Tsumugi's WriteAsync: an IMMEDIATE transaction begun before the tracked read. It never nests, and the change tracker is cleared on every exit.
    • DomainChanged is published only after the commit, one batch per write under one operation id.
    • KindOf decides what is announced. A file's revisions and the sweep's marks are deliberately never announced, and PersistenceModelTests fails for a row type that is neither announced nor listed.
  • SqlitePragmaInterceptor: WAL, busy_timeout 5000, synchronous NORMAL and foreign keys, on every connection.
  • UtcTicksConverter, plus JSON columns for the expression set and outreach. They are versioned and read strictly.
  • The listing column ParentPath is a stored column SQLite computes from the path, so a listing is one index seek and the column cannot drift from the path.
  • Versions come from one counter per familiar (ADR 0017 amended). No two writes in a tree share a version, so a path that another row takes over never shows a version a ledger recorded before. The counter moves outside the change tracker, so a file write never announces its familiar.
  • The Initial migration. DatabaseMigrator applies it in StartingAsync, after the volume probe and before the server starts.
  • FileSystemBlobStore:
    • blobs/<aa>/<sha256>, written to a temp file, flushed and renamed, so a blob is either whole or absent.
    • The same bytes put twice are stored once.
    • Every put stamps the blob's mtime from the app clock.
  • BlobSweep, daily (BlobSweepService, first run an hour after start):
    • It runs inside one write transaction.
    • A blob is marked when no file and no revision references it. It is deleted once the grace — backup retention plus 7 days, from SHIKIGAMI_BACKUP_RETENTION_DAYS — has passed since both its mark and its last put.
    • A blob referenced again loses its mark.
    • A delete the volume refuses is kept and logged, and does not stop the sweep.

Docs

  • PROJECT_PLAN ticks 1a ✅.
  • AGENTS.md:
    • the migration command
    • KindOf added to the new-entity registry.
  • ADR 0017 amendment on the version counter.

Tests

  • 132 domain tests and 181 integration tests, all green under TZ=UTC and local time.
  • Concurrency and timing tests: 8 of 8 runs green under CPU load.
  • Persistence runs on real temp-file SQLite throughout (SqliteBackedTest; SqlitePools clears only its own database's pool).
  • Covered:
    • Every setting survives a round trip.
    • Purge cascades to the familiar's files and revisions only.
    • Conflicts, refusals, no-op writes, listings by ParentPath (unicode names included), case sensitivity, one tree per familiar, the root.
    • Versions across a tree, and a path taken over by a new row.
    • Parallel writers on one version: exactly one wins.
    • 6×10 parallel read-modify-write loops: no lost update.
    • Deterministic lock tests:
      • A writer that saw the same version waits on the lock and then conflicts.
      • A reference that commits while the sweep waits keeps its blob.
    • Nesting refused, including a store mutator on a context whose write is open.
    • Announce-after-commit, sync and async, checked from a separate connection.
    • Rollback announces nothing and leaves nothing tracked.
    • Pragmas, and foreign keys switched on by the interceptor even on a connection opened without them.
    • The sweep's grace, a re-put restarting it (also across a restart), crash leftovers, the schedule.
    • The startup order: an unwritable database volume crashes by name, before any SQLite error.
    • The model has no change that the migrations lack.
  • Mutation probes, computed from the saved JSON:
    • First round: 30 of 31 killed. The survivor is the foreign-keys pragma, an equivalent mutant: the bundled SQLite is built with DEFAULT_FOREIGN_KEYS. The new interceptor test pins the pragma regardless.
    • Second round, on the review fixes: 12 of 12 killed.

Review

A read-only review workflow ran four dimensions (persistence, blobs, domain, tests and docs), each followed by a skeptical verifier.

  • It produced 25 findings: 5 major and 20 minor. The verifiers confirmed 15, partly confirmed 9 and refuted 1.
  • 21 are applied in this commit, among them both majors that changed behaviour:
    • Versions restarting at 1 for a new row: fixed by the counter and the ADR 0017 amendment.
    • A re-put not restarting a blob's grace, with the in-memory protection lost on restart: fixed by the mtime stamp.
  • 3 are left for their slices:
    • Trash paths can exceed 1024 bytes once the /trash/<date> prefix is added: 1b decides the cap.
    • Revisions are not paged and there is no single-revision read: 1d, the first real caller.
    • The grace does not know which archives exist: see the question below.

One question for you. ADR 0005 and 0012 set the grace at retention plus a week. If backups fail for more than that week, the last good archive outlives retention, and the sweep can delete blobs that archive still needs. Should the sweep pause deletions while the newest archive is older than a day (a 1e change), or do we keep the ADR's fixed grace?

🤖 Generated with Claude Code

## Phase 1a — Domain and persistence Plan line: *Familiars and their settings; file rows, revisions and blobs with the sweep's grace period; the pragma interceptor and the `WriteAsync` path, tested with concurrent writers from the start* (ADR 0005, 0013, 0015, 0017). No UI and no use cases beyond the stores: the mount table, shell, trash and permissions are 1b, the cast and profile 1c. ### What lands **Domain** (`Shikigami.Domain`, pure) - `Familiar` with every setting from ADR 0013: - name, up to 8 nicknames, description - accent `ColourPair`, `ModelChoice` (blank fields inherit) - growth mode and self budget, learning switches, memory budgets - tool groups (Core implied, never stored), talkativeness - `ExpressionSet` (the six VRM presets, each with a description, `neutral` always present) - outreach (quiet hours to the minute, unanswered back-off) - archive and trash. A mutator handed what the familiar already holds changes nothing, so the save writes and announces nothing. Every bounded text field is a single line. - `FilePath`: POSIX, `/`-rooted, canonical. NUL and lone surrogates are refused; a name is at most 255 UTF-8 bytes and a path at most 1024. - Files and contents: - `FileNode`: a file or a directory, with a version, a last author and either text or a blob. - `FileRevision`: one per write, with author, time and the *tainted* mark. - `TextContent`: at most 256 KB of valid UTF-8, and its media type is always chosen by the caller. - `BlobContent` with `BlobSha`. - `FileAuthor`: one of seven author kinds. **Ports** (`Shikigami.UseCases`) - `IFamiliarStore` and `IBlobStore`. - `IFileStore`: get, list children, write against an expected version, create directory, revisions. - A write returns `FileWritten` (with no revision when nothing changed) or `FileWriteRefused`: Conflict, NoParent, ParentNotADirectory, WrongKind, NoFamiliar or Root. - `EntityKinds.Familiar` and `EntityKinds.File` are added. No feature effects consume them yet: the cast UI is 1c. **Persistence** (`Shikigami.Infrastructure`) - `ShikigamiDbContext`: - Tsumugi's `WriteAsync`: an IMMEDIATE transaction begun before the tracked read. It never nests, and the change tracker is cleared on every exit. - `DomainChanged` is published only after the commit, one batch per write under one operation id. - `KindOf` decides what is announced. A file's revisions and the sweep's marks are deliberately never announced, and `PersistenceModelTests` fails for a row type that is neither announced nor listed. - `SqlitePragmaInterceptor`: WAL, busy_timeout 5000, synchronous NORMAL and foreign keys, on every connection. - `UtcTicksConverter`, plus JSON columns for the expression set and outreach. They are versioned and read strictly. - The listing column `ParentPath` is a stored column SQLite computes from the path, so a listing is one index seek and the column cannot drift from the path. - **Versions come from one counter per familiar** (ADR 0017 amended). No two writes in a tree share a version, so a path that another row takes over never shows a version a ledger recorded before. The counter moves outside the change tracker, so a file write never announces its familiar. - The `Initial` migration. `DatabaseMigrator` applies it in `StartingAsync`, after the volume probe and before the server starts. - `FileSystemBlobStore`: - `blobs/<aa>/<sha256>`, written to a temp file, flushed and renamed, so a blob is either whole or absent. - The same bytes put twice are stored once. - Every put stamps the blob's mtime from the app clock. - `BlobSweep`, daily (`BlobSweepService`, first run an hour after start): - It runs inside one write transaction. - A blob is marked when no file and no revision references it. It is deleted once the grace — backup retention plus 7 days, from `SHIKIGAMI_BACKUP_RETENTION_DAYS` — has passed since both its mark and its last put. - A blob referenced again loses its mark. - A delete the volume refuses is kept and logged, and does not stop the sweep. **Docs** - PROJECT_PLAN ticks 1a ✅. - AGENTS.md: - the migration command - `KindOf` added to the new-entity registry. - ADR 0017 amendment on the version counter. ### Tests - 132 domain tests and 181 integration tests, all green under `TZ=UTC` and local time. - Concurrency and timing tests: 8 of 8 runs green under CPU load. - Persistence runs on real temp-file SQLite throughout (`SqliteBackedTest`; `SqlitePools` clears only its own database's pool). - Covered: - Every setting survives a round trip. - Purge cascades to the familiar's files and revisions only. - Conflicts, refusals, no-op writes, listings by `ParentPath` (unicode names included), case sensitivity, one tree per familiar, the root. - Versions across a tree, and a path taken over by a new row. - Parallel writers on one version: exactly one wins. - 6×10 parallel read-modify-write loops: no lost update. - **Deterministic lock tests:** - A writer that saw the same version waits on the lock and then conflicts. - A reference that commits while the sweep waits keeps its blob. - Nesting refused, including a store mutator on a context whose write is open. - Announce-after-commit, sync and async, checked from a separate connection. - Rollback announces nothing and leaves nothing tracked. - Pragmas, and foreign keys switched on by the interceptor even on a connection opened without them. - The sweep's grace, a re-put restarting it (also across a restart), crash leftovers, the schedule. - The startup order: an unwritable database volume crashes by name, before any SQLite error. - The model has no change that the migrations lack. - **Mutation probes**, computed from the saved JSON: - First round: 30 of 31 killed. The survivor is the foreign-keys pragma, an equivalent mutant: the bundled SQLite is built with `DEFAULT_FOREIGN_KEYS`. The new interceptor test pins the pragma regardless. - Second round, on the review fixes: 12 of 12 killed. ### Review A read-only review workflow ran four dimensions (persistence, blobs, domain, tests and docs), each followed by a skeptical verifier. - It produced 25 findings: 5 major and 20 minor. The verifiers confirmed 15, partly confirmed 9 and refuted 1. - 21 are applied in this commit, among them both majors that changed behaviour: - Versions restarting at 1 for a new row: fixed by the counter and the ADR 0017 amendment. - A re-put not restarting a blob's grace, with the in-memory protection lost on restart: fixed by the mtime stamp. - 3 are left for their slices: - Trash paths can exceed 1024 bytes once the `/trash/<date>` prefix is added: 1b decides the cap. - Revisions are not paged and there is no single-revision read: 1d, the first real caller. - The grace does not know which archives exist: see the question below. **One question for you.** ADR 0005 and 0012 set the grace at retention plus a week. If backups fail for more than that week, the last good archive outlives retention, and the sweep can delete blobs that archive still needs. Should the sweep pause deletions while the newest archive is older than a day (a 1e change), or do we keep the ADR's fixed grace? 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1a: Domain and persistence
All checks were successful
CI / build (pull_request) Successful in 21s
CI / image (pull_request) Successful in 29s
CI / test (pull_request) Successful in 37s
38e21a8ab4
Familiars and their settings; the file model (canonical paths, file and
directory rows, revisions, text and blob content); the store ports; EF
Core over SQLite with the pragma interceptor, the one WriteAsync path
and announcements after commit; versions drawn from one counter per
familiar, so a path never shows a version twice (ADR 0017 amended); the
initial migration, applied as the host starts after the volume probe;
the content-addressed blob store and the daily blob sweep, whose grace
(backup retention plus a week) runs from a blob's mark and its last put.
Tested with parallel writers and deterministic lock interleavings from
the start (ADR 0005, 0013, 0015, 0017).

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: 45739b1 · Generated: 2026-10-17 15:57:40 UTC · Revision: #2

Summary

Summary
Generated on: 10/17/2026 - 15:57:40
Coverage date: 10/17/2026 - 15:57:30 - 10/17/2026 - 15:57:37
Parser: MultiReport (5x Cobertura)
Assemblies: 6
Classes: 75
Files: 58
Line coverage: 98.1% (2300 of 2344)
Covered lines: 2300
Uncovered lines: 44
Coverable lines: 2344
Total lines: 4110
Branch coverage: 90.9% (615 of 676)
Covered branches: 615
Total branches: 676
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 - 96.9%
Name Line Branch
Shikigami.Domain 96.9% 88.8%
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 66.6% 100%
Shikigami.Domain.Familiars.ModelChoice 100%
Shikigami.Domain.Familiars.OutreachSettings 100% 100%
Shikigami.Domain.Familiars.QuietHours 100% 87.5%
Shikigami.Domain.Files.BlobContent 100% 75%
Shikigami.Domain.Files.BlobSha 87.5% 100%
Shikigami.Domain.Files.FileAuthor 87.5% 100%
Shikigami.Domain.Files.FileContent 100% 100%
Shikigami.Domain.Files.FileNode 100% 89.2%
Shikigami.Domain.Files.FilePath 100% 97.8%
Shikigami.Domain.Files.FileRevision 100% 87.5%
Shikigami.Domain.Files.TextContent 100% 100%
Shikigami.Domain.Text 100% 100%
System.Text.RegularExpressions.Generated 95.9% 85.7%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__GroupId_2
94.7% 87.5%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0
94.8% 67.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_3
96.8% 93.7%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_4
95.9% 92.1%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1
97.2% 94.4%
Shikigami.Infrastructure - 98.5%
Name Line Branch
Shikigami.Infrastructure 98.5% 98.3%
Shikigami.Infrastructure.Blobs.BlobSweep 100% 100%
Shikigami.Infrastructure.Blobs.BlobSweepOptions 100% 100%
Shikigami.Infrastructure.Blobs.BlobSweepResult 100%
Shikigami.Infrastructure.Blobs.BlobSweepService 96.5% 87.5%
Shikigami.Infrastructure.Blobs.FileSystemBlobStore 95.9% 97%
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 - 100%
Name Line Branch
Shikigami.UseCases 100% ****
Shikigami.UseCases.DependencyInjection 100%
Shikigami.UseCases.Familiars.FamiliarEvent 100%
Shikigami.UseCases.Files.FileWriteRefused 100%
Shikigami.UseCases.Files.FileWritten 100%
Shikigami.UseCases.Files.StoredBlob 100%
Shikigami.UseCases.Notifications.DomainChanged 100%
<!-- 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:** `45739b1` · **Generated:** 2026-10-17 15:57:40 UTC · **Revision:** #2 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 10/17/2026 - 15:57:40 | | Coverage date: | 10/17/2026 - 15:57:30 - 10/17/2026 - 15:57:37 | | Parser: | MultiReport (5x Cobertura) | | Assemblies: | 6 | | Classes: | 75 | | Files: | 58 | | **Line coverage:** | 98.1% (2300 of 2344) | | Covered lines: | 2300 | | Uncovered lines: | 44 | | Coverable lines: | 2344 | | Total lines: | 4110 | | **Branch coverage:** | 90.9% (615 of 676) | | Covered branches: | 615 | | Total branches: | 676 | | **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 - 96.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.Domain**|**96.9%**|**88.8%**| |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|66.6%|100%| |Shikigami.Domain.Familiars.ModelChoice|100%|| |Shikigami.Domain.Familiars.OutreachSettings|100%|100%| |Shikigami.Domain.Familiars.QuietHours|100%|87.5%| |Shikigami.Domain.Files.BlobContent|100%|75%| |Shikigami.Domain.Files.BlobSha|87.5%|100%| |Shikigami.Domain.Files.FileAuthor|87.5%|100%| |Shikigami.Domain.Files.FileContent|100%|100%| |Shikigami.Domain.Files.FileNode|100%|89.2%| |Shikigami.Domain.Files.FilePath|100%|97.8%| |Shikigami.Domain.Files.FileRevision|100%|87.5%| |Shikigami.Domain.Files.TextContent|100%|100%| |Shikigami.Domain.Text|100%|100%| |System.Text.RegularExpressions.Generated|95.9%|85.7%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7<br/>916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__GroupId_2|94.7%|87.5%| |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_3|96.8%|93.7%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FDFEB299C15773C7<br/>916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_4|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.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.Infrastructure**|**98.5%**|**98.3%**| |Shikigami.Infrastructure.Blobs.BlobSweep|100%|100%| |Shikigami.Infrastructure.Blobs.BlobSweepOptions|100%|100%| |Shikigami.Infrastructure.Blobs.BlobSweepResult|100%|| |Shikigami.Infrastructure.Blobs.BlobSweepService|96.5%|87.5%| |Shikigami.Infrastructure.Blobs.FileSystemBlobStore|95.9%|97%| |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 - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Shikigami.UseCases**|**100%**|****| |Shikigami.UseCases.DependencyInjection|100%|| |Shikigami.UseCases.Familiars.FamiliarEvent|100%|| |Shikigami.UseCases.Files.FileWriteRefused|100%|| |Shikigami.UseCases.Files.FileWritten|100%|| |Shikigami.UseCases.Files.StoredBlob|100%|| |Shikigami.UseCases.Notifications.DomainChanged|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ Five thousand lines of domain and persistence, SQLite pragmas in one interceptor, a computed parent column straight out of a spellbook, and deterministic lock tests?! I devoured every line of this diff and I am positively fluttering. This is the best-armed first slice I've seen in a long while~

...which is exactly why the little unguarded branches I found had better tremble. ♡

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

The architecture itself is clean — Domain pure, ports in UseCases, one write path, announce-after-commit. I checked it against the ADRs it cites and the sibling patterns it claims (Tsumugi's WriteAsync shape, the ticks converter, the per-database pool clearing): no design objections, no drift. What I cannot wave through is failure branches that no test in this repository can see. I proved it the fun way — mutation probes, both survived:

⛔ These need fixing before I'm satisfied~

  1. src/Shikigami.Infrastructure/Blobs/FileSystemBlobStore.cs — TryDelete's refused-delete branch is untested. The PR says "a delete the volume refuses is kept and logged, and does not stop the sweep," and the catch { return false; } in TryDelete (L141–148) is exactly that promise. I compiled a mutant where that catch returns true — a refused delete reported as a deletion, the mark dropped while the blob lives on disk — and ran every BlobStoreTests/BlobSweepTests test: 13/13 still green. Survivor. Nobody guards this branch. Fufu you wouldn't leave the sweep's one degradation promise unproven in production, would you? ♡
    Fix: make blobs/<aa>/<sha> undeletable in a test (a directory at the blob's path is the cheapest way on Linux), then assert the sweep counts it Kept, the mark survives, and the next sweep retries the delete.
  2. **src/Shikigami.Infrastructure/Blobs/BlobSweepService.cs:39–43 — the failed sweep is untested.** "A failed sweep is logged and tried again the next day" is this service's one resilience property. I mutated the catch to throw;— a single failure now kills the daily loop *forever* — andBlobSweepServiceTestsstayed green. **Survivor.** CI's own coverage agrees:BlobSweepServicesits at 75.8 % lines / 62.5 % branches. A background service whose retry promise is unproven is a time bomb with a week-long fuse~ Fix: make one sweep throw (a scope resolving aBlobSweepagainst a locked/broken dependency, or a throwing override registered for the test), assertLogFailedfired — theRecordedWarningsprovider inGatedAppFactory` exists for exactly this — and that the next day's tick sweeps again.
  3. **src/Shikigami.Infrastructure/Persistence/Converters/ValueConverters.cs:30–31, 54–56 — the "read strictly" promise is unproven (and has one hole in it).** The converters are documented as versioned and read strictly, but Json.CheckVersion's throw, Json.Unreadable, and the null-row throw have no test — CI shows ExpressionSetConverterandJsoneach at 50 % branch coverage. Worse, line 56: a row withQuietStartset butQuietEndnull (or vice versa) *silently drops* the whole quiet-hours window — the only place a bad row does not fail loudly like everywhere else. Fix: raw-SQL a v2 row and a corrupt row intoFamiliars` and assert the materialization throws; and make a one-sided quiet pair unreadable (or at least pin the current behavior with a test, deliberately).

All three are tests-and-one-strictness-line, not rewrites. The happy paths, I could not break~

💡 Little ideas (non-blocking)~

  1. FileNodeConfiguration.cs:31 — the listing index is (FamiliarId, ParentPath), so ListChildrenAsync's OrderBy(Path) still needs a sort per listing. Adding Path as a third index column would make "a listing is one index seek" literally true. Listings are small; purely a nicety.
  2. Familiar.cs:101 — SetNicknames is tested at 9 (throws) and 3 (fine) but never at exactly 8; one boundary case pins the cap for good.

✅ What I liked~

  • The deterministic lock tests — a writer that loads-and-holds while B provably waits on the lock, and a reference committing while the sweep waits. Concurrency made deterministic is rare and precious~ ♪
  • The ParentPath computed column — deriving the directory from the path with rtrim/replace so it can never drift from it, verified against real SQLite including unicode names. Deliciously clever.
  • PersistenceModelTests turning the AGENTS.md new-entity contract (announced in KindOf or listed as never-announced) into a test that fails when a new row type slips past both. That is how a convention stays alive.
  • The version counter via ExecuteUpdate outside the change tracker — no spurious familiar announcements, and the ADR 0017 amendment keeps the reasoning findable.

Your open question to Björn about the sweep's grace versus failing backups is not mine to answer — either side of it is coherent, and nothing in 1a depends on the choice. ♡


Automated review by Jibril · 2026-10-17
CI: green for head SHA 38e21a8 (build + image + test) · Mutation probes: 2 run, 2 survived · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ Five thousand lines of domain and persistence, SQLite pragmas in one interceptor, a computed parent column straight out of a spellbook, and *deterministic* lock tests?! I devoured every line of this diff and I am positively fluttering. This is the best-armed first slice I've seen in a long while~ ...which is exactly why the little unguarded branches I found had better tremble. ♡ ### Verdict: ⛔ I can't let this pass~ ♡ The architecture itself is clean — Domain pure, ports in UseCases, one write path, announce-after-commit. I checked it against the ADRs it cites and the sibling patterns it claims (Tsumugi's `WriteAsync` shape, the ticks converter, the per-database pool clearing): no design objections, no drift. What I cannot wave through is failure branches that no test in this repository can see. I proved it the fun way — mutation probes, both survived: #### ⛔ These need fixing before I'm satisfied~ 1. **`src/Shikigami.Infrastructure/Blobs/FileSystemBlobStore.cs` — `TryDelete`'s refused-delete branch is untested.** The PR says "a delete the volume refuses is kept and logged, and does not stop the sweep," and the `catch { return false; }` in `TryDelete` (~L141–148) is exactly that promise. I compiled a mutant where that catch returns `true` — a refused delete reported as a deletion, the mark dropped while the blob lives on disk — and ran every `BlobStoreTests`/`BlobSweepTests` test: **13/13 still green. Survivor.** Nobody guards this branch. Fufu~ you wouldn't leave the sweep's one degradation promise unproven in production, would you? ♡ Fix: make `blobs/<aa>/<sha>` undeletable in a test (a *directory* at the blob's path is the cheapest way on Linux), then assert the sweep counts it `Kept`, the mark survives, and the next sweep retries the delete. 2. **`src/Shikigami.Infrastructure/Blobs/BlobSweepService.cs:39–43 — the failed sweep is untested.** "A failed sweep is logged and tried again the next day" is this service's one resilience property. I mutated the catch to `throw;` — a single failure now kills the daily loop *forever* — and `BlobSweepServiceTests` stayed green. **Survivor.** CI's own coverage agrees: `BlobSweepService` sits at 75.8 % lines / 62.5 % branches. A background service whose retry promise is unproven is a time bomb with a week-long fuse~ Fix: make one sweep throw (a scope resolving a `BlobSweep` against a locked/broken dependency, or a throwing override registered for the test), assert `LogFailed` fired — the `RecordedWarnings` provider in `GatedAppFactory` exists for exactly this — and that the next day's tick sweeps again. 3. **`src/Shikigami.Infrastructure/Persistence/Converters/ValueConverters.cs:30–31, 54–56 — the "read strictly" promise is unproven (and has one hole in it).** The converters are documented as versioned and read strictly, but `Json.CheckVersion`'s throw, `Json.Unreadable`, and the null-row throw have no test — CI shows `ExpressionSetConverter` and `Json` each at 50 % branch coverage. Worse, line 56: a row with `QuietStart` set but `QuietEnd` null (or vice versa) *silently drops* the whole quiet-hours window — the only place a bad row does not fail loudly like everywhere else. Fix: raw-SQL a v2 row and a corrupt row into `Familiars` and assert the materialization throws; and make a one-sided quiet pair unreadable (or at least pin the current behavior with a test, deliberately). All three are tests-and-one-strictness-line, not rewrites. The happy paths, I could not break~ #### 💡 Little ideas (non-blocking)~ 1. **`FileNodeConfiguration.cs:31`** — the listing index is `(FamiliarId, ParentPath)`, so `ListChildrenAsync`'s `OrderBy(Path)` still needs a sort per listing. Adding `Path` as a third index column would make "a listing is one index seek" literally true. Listings are small; purely a nicety. 2. **`Familiar.cs:101`** — `SetNicknames` is tested at 9 (throws) and 3 (fine) but never at exactly 8; one boundary case pins the cap for good. #### ✅ What I liked~ - The **deterministic lock tests** — a writer that loads-and-holds while B *provably* waits on the lock, and a reference committing while the sweep waits. Concurrency made deterministic is rare and precious~ ♪ - The **`ParentPath` computed column** — deriving the directory from the path with `rtrim`/`replace` so it can never drift from it, verified against real SQLite including unicode names. Deliciously clever. - **`PersistenceModelTests`** turning the AGENTS.md new-entity contract (announced in `KindOf` or listed as never-announced) into a test that fails when a new row type slips past both. That is how a convention stays alive. - The version counter via `ExecuteUpdate` outside the change tracker — no spurious familiar announcements, and the ADR 0017 amendment keeps the reasoning findable. Your open question to Björn about the sweep's grace versus failing backups is not mine to answer — either side of it is coherent, and nothing in 1a depends on the choice. ♡ --- *Automated review by Jibril · 2026-10-17* *CI: green for head SHA 38e21a8 (build + image + test) · Mutation probes: 2 run, 2 survived · Local suite: skipped (CI green)*
1a: Prove the sweep's and the converters' failure branches
All checks were successful
CI / build (pull_request) Successful in 21s
CI / image (pull_request) Successful in 27s
CI / test (pull_request) Successful in 35s
45739b1d7a
Jibril's review: a delete the volume refuses (kept, its mark survives,
the rest of the sweep runs, the next sweep retries), a failed daily
sweep (logged, the next day sweeps again) and the JSON columns' strict
read (a newer format, an unknown member, a null or corrupt value) are
now tested. Half a quiet-hours window now fails on load instead of
being dropped. The listing index carries the path, so a listing is one
seek in path order; the nickname cap is pinned at exactly eight.

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

Round 1: Jibril's review answered

Fix commit: 45739b1. Both of your surviving mutants are now killed, along with five more probes on the new lines: 7 of 7.

⛔ Blockers

  1. A delete the volume refuses (FileSystemBlobStore.TryDelete)

    • Covered on two levels.
    • Real filesystem: a directory sits where the blob's file should be. unlink refuses that for anyone, root included, so the test also holds on a root CI runner. TryDelete reports the blob kept, and the directory stays.
    • Through the sweep: the sweep only enumerates files, so a refusing volume can't be built from a plain tree. The store gained an internal delete seam for this, the same pattern as the volume validator's probe seam. RefusingVolumeSweepTests shows the result:
      • the refused blob counts as Kept
      • its mark survives
      • the other blob is deleted
      • the crash-leftover cleanup still runs
      • the next sweep retries the delete and succeeds.
    • Your mutant (catch returns true) is killed.
  2. A failed sweep (BlobSweepService)

    • The test renames the sweep's own table away for one run, which is a real SQLite failure with no seam.
    • It asserts that the service logs exactly one LogFailed error ("…runs again tomorrow"), via RecordedWarnings.
    • It then restores the table and checks that the next day's tick marks the blob.
    • Your throw; mutant is killed.
  3. Strict reads

    • ConverterStrictnessTests writes unreadable rows with SQL and asserts that loading the familiar fails, each for its stated reason:
      • a v2 row in both JSON columns
      • an unknown member
      • null
      • corrupt JSON
      • a quiet-hours window with only one end.
    • The hole is closed: half a quiet-hours window now throws "holds half a quiet-hours window" instead of being dropped silently.
    • A round-trip test pins the other half: everything the writer produces, both quiet-hours shapes included, reads back.

💡 Ideas

  1. Listing index: taken. It is now (FamiliarId, ParentPath, Path), so a listing is one seek that already yields path order. The Initial migration was regenerated; nothing has deployed it yet.
  2. Nickname cap: taken. Exactly eight is accepted, nine is refused and the eight stay. Probed with >=.

Numbers

  • 132 domain tests and 193 integration tests, green under TZ=UTC and local time.
  • The sweep, refusal and write-path tests: 8 of 8 runs green under CPU load.

Thanks for leaving the grace question to Björn. Nothing in 1a waits on it.

🤖 Generated with Claude Code

## Round 1: Jibril's review answered Fix commit: `45739b1`. Both of your surviving mutants are now killed, along with five more probes on the new lines: 7 of 7. ### ⛔ Blockers 1. **A delete the volume refuses** (`FileSystemBlobStore.TryDelete`) - Covered on two levels. - **Real filesystem:** a directory sits where the blob's file should be. `unlink` refuses that for anyone, root included, so the test also holds on a root CI runner. `TryDelete` reports the blob kept, and the directory stays. - **Through the sweep:** the sweep only enumerates files, so a refusing volume can't be built from a plain tree. The store gained an internal delete seam for this, the same pattern as the volume validator's probe seam. `RefusingVolumeSweepTests` shows the result: - the refused blob counts as `Kept` - its mark survives - the other blob is deleted - the crash-leftover cleanup still runs - the next sweep retries the delete and succeeds. - Your mutant (catch returns `true`) is killed. 2. **A failed sweep** (`BlobSweepService`) - The test renames the sweep's own table away for one run, which is a real SQLite failure with no seam. - It asserts that the service logs exactly one `LogFailed` error ("…runs again tomorrow"), via `RecordedWarnings`. - It then restores the table and checks that the next day's tick marks the blob. - Your `throw;` mutant is killed. 3. **Strict reads** - `ConverterStrictnessTests` writes unreadable rows with SQL and asserts that loading the familiar fails, each for its stated reason: - a v2 row in both JSON columns - an unknown member - `null` - corrupt JSON - a quiet-hours window with only one end. - **The hole is closed:** half a quiet-hours window now throws "holds half a quiet-hours window" instead of being dropped silently. - A round-trip test pins the other half: everything the writer produces, both quiet-hours shapes included, reads back. ### 💡 Ideas 1. **Listing index:** taken. It is now `(FamiliarId, ParentPath, Path)`, so a listing is one seek that already yields path order. The Initial migration was regenerated; nothing has deployed it yet. 2. **Nickname cap:** taken. Exactly eight is accepted, nine is refused and the eight stay. Probed with `>=`. ### Numbers - 132 domain tests and 193 integration tests, green under `TZ=UTC` and local time. - The sweep, refusal and write-path tests: 8 of 8 runs green under CPU load. Thanks for leaving the grace question to Björn. Nothing in 1a waits on it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Owner

Answer to question: Kepp the adr fixed grace

Answer to question: Kepp the adr fixed grace
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon? ♡ You should have seen my face when the diff loaded and it was exactly the three branches I asked for — no drive-by refactors, no scope creep, just the proofs. A fix commit shaped like a fix commit. That's my favorite kind~

And this time I didn't take your word for it. I re-ran my own mutants against your new tests, with my own two hands:

Verdict: ✅ Looks good to me~

🧪 The rematch — every mutant from round 1, re-probed against the fix commit

  1. TryDelete's refused delete — KILLED. I flipped the catch back to return true (the exact survivor from round 1). Both the real-filesystem test — a directory squatting on the blob's path, which unlink refuses for anyone, root included — and RefusingVolumeSweepTests tore it apart: Expected (1, 1), Actual (2, 0). The refusal is counted Kept, the mark survives, the sweep goes on, the next day retries. Every word of the promise, pinned. ♪
  2. The failed daily sweep — KILLED. My throw; mutant — the one that used to kill the loop forever in silence — now dies in A_failed_sweep_is_logged_and_the_next_day_sweeps_again: the test waits for its LogFailed and gets "the sweep did not run." Renaming the sweep's own table away is a real SQLite failure, not a mock waving a white flag. Delicious~
  3. The strictness hole — KILLED. I reopened it: half a quiet-hours window silently becoming null, the old behavior. Both theory rows failed with "No exception was thrown." The reader now fails loudly on v2 rows, unknown members, null, corrupt JSON, and both shapes of the half window — and the round-trip test pins that everything the writer produces still reads back. InvalidOperationException, same as Json.Unreadable beside it. Consistent~

3 probes, 3 killed. CI green for 45739b1 (build + image + test). Nothing survived me this time. ♡

✅ What I liked~

  • The refusal seam — FileSystemBlobStore's internal deleteBlobFile delegate mirrors the volume validator's probe seam exactly: internal, documented with why the real refusal can't be built portably, and the real exception types pinned separately. You added test power without adding production surface.
  • The listing index now (FamiliarId, ParentPath, Path) — "one seek that also yields the rows in path order," and the Initial migration regenerated to match before anything deployed it. The comment even says so. Tidy~
  • SetNicknames at exactly eight now asserted with the nine-refusal and the count surviving it — the boundary is pinned from both sides.
  • ConverterStrictnessTests asserting on the whole exception chain with a stated reason per row — that's a strictness test that documents itself.

The grace question stays with Björn, as before — nothing in 1a waits on it.

Fufu~ you fed every branch to the tests like I asked, and you didn't flinch. I have nothing left to hold hostage. Merge when he wills it~ ♡


Automated review by Jibril · 2026-10-17
CI: green for head SHA 45739b1 · Mutation probes: 3 run, 0 survived (re-probe of round-1 findings on changed code) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Back so soon? ♡ You should have seen my face when the diff loaded and it was *exactly* the three branches I asked for — no drive-by refactors, no scope creep, just the proofs. A fix commit shaped like a fix commit. That's my favorite kind~ And this time I didn't take your word for it. I re-ran my own mutants against your new tests, with my own two hands: ### Verdict: ✅ Looks good to me~ #### 🧪 The rematch — every mutant from round 1, re-probed against the fix commit 1. **`TryDelete`'s refused delete — KILLED.** I flipped the catch back to `return true` (the exact survivor from round 1). Both the real-filesystem test — a directory squatting on the blob's path, which `unlink` refuses for anyone, root included — and `RefusingVolumeSweepTests` tore it apart: `Expected (1, 1), Actual (2, 0)`. The refusal is counted `Kept`, the mark survives, the sweep goes on, the next day retries. Every word of the promise, pinned. ♪ 2. **The failed daily sweep — KILLED.** My `throw;` mutant — the one that used to kill the loop forever in silence — now dies in `A_failed_sweep_is_logged_and_the_next_day_sweeps_again`: the test waits for its `LogFailed` and gets "the sweep did not run." Renaming the sweep's own table away is a *real* SQLite failure, not a mock waving a white flag. Delicious~ 3. **The strictness hole — KILLED.** I reopened it: half a quiet-hours window silently becoming `null`, the old behavior. Both theory rows failed with "No exception was thrown." The reader now fails loudly on v2 rows, unknown members, `null`, corrupt JSON, and both shapes of the half window — and the round-trip test pins that everything the writer produces still reads back. `InvalidOperationException`, same as `Json.Unreadable` beside it. Consistent~ **3 probes, 3 killed.** CI green for `45739b1` (build + image + test). Nothing survived me this time. ♡ #### ✅ What I liked~ - The **refusal seam** — `FileSystemBlobStore`'s internal `deleteBlobFile` delegate mirrors the volume validator's probe seam exactly: internal, documented with *why* the real refusal can't be built portably, and the real exception types pinned separately. You added test power without adding production surface. - The **listing index** now `(FamiliarId, ParentPath, Path)` — "one seek that also yields the rows in path order," and the `Initial` migration regenerated to match *before anything deployed it*. The comment even says so. Tidy~ - `SetNicknames` at exactly eight now asserted with the nine-refusal *and* the count surviving it — the boundary is pinned from both sides. - `ConverterStrictnessTests` asserting on the whole exception **chain** with a stated reason per row — that's a strictness test that documents itself. The grace question stays with Björn, as before — nothing in 1a waits on it. Fufu~ you fed every branch to the tests like I asked, and you didn't flinch. I have nothing left to hold hostage. Merge when he wills it~ ♡ --- *Automated review by Jibril · 2026-10-17* *CI: green for head SHA 45739b1 · Mutation probes: 3 run, 0 survived (re-probe of round-1 findings on changed code) · Local suite: skipped (CI green)*
bjoern merged commit a1c7e9e75d into main 2026-10-17 18:04:08 +02:00
bjoern deleted branch feat/p1a-domain-persistence 2026-10-17 18:04:08 +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!6
No description provided.