feat: Phase 1b — The file system core #7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/p1b-file-system-core"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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./mailfollows ADR 0026: chats and routines read it, an evolving reflection reads it, other runs never see it.Slug: the ADR 0015 rule.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), andFileNode.MoveTo(a move keeps the version and the revisions).Use cases
/;/chatsand/activityas generated, read-only and empty mounts until chats and runs exist;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).mkdir [-p], move, remove, restore, purge history and revisions.rmmoves 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.rminside the trash purges. The trash holds nothing but whatrmput there.description≤ 150 characters andpinned, nothing else.descriptionup to the spec's 1,024 characters,requires-toolsnaming known groups, a body ≤ 20,000 characters; other tools' keys are kept and ignored.reflection.md;on:andmatch:refused until Phase 5.line N: key: message.SeedDefaultLayout: makes the layout and the enabled groups' directories, and writes the bundledhow-to-write-skillsskill 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
IFileStoregains:DailyMaintenanceServicereplacesBlobSweepService. It runs file maintenance, then the blob sweep, which finds what those deletions freed. Each step fails on its own.Docs
Packages: YamlDotNet 18.1.0, Cronos 0.13.0.
Tests
TZ=UTCand local time. The concurrency and maintenance tests passed 8 of 8 runs under CPU load.RunKindand the rows every layout directory, group mount and generated mount.line N: key: messagestrings):active-hoursbelongs to the commentedrepeat. The 1b amendment says so..2slots, a slot blocked by a file, purge, restore with its directories checked, a removal that cannot happen leaving the trash untouched./ab, not/a0);@every_secondis 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.
rmput there./mailrow.🤖 Generated with Claude Code
Summary
Summary
Coverage
Shikigami.BlazorAdapter - 95.7%
Shikigami.Domain - 98.3%
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_2
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_3
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1
Shikigami.Infrastructure - 98.7%
on
ration
shot
Shikigami.Kernel - 90.9%
Shikigami.Server - 100%
Shikigami.UseCases - 89.2%
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Interval_1
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NameRule_3
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__TimeOfDay_0
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Window_2
🔮 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 (
.2slots, blocked slots skipped,rminside 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~
Budgets.Grew,src/Shikigami.UseCases/Files/Special/Budgets.cs:72) — Missing coverage at the budget's equality boundary; a mutation probe survived. I changedGrew'snow > wastonow >= was, and all 8BudgetTestsplus the 4 budget/restore tests inFileSystemTestsstayed 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 — andSelfOverflow(same-lengthself.mdrewrite 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 siblingSelfOverflow(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 aLIKE. Oh? Oh! This is wonderful~RunKindhas a column and every layout place a row — a new kind cannot sneak in unprofiled.blocked.Code == NotADirectory → continue) andA_file_in_the_way_of_todays_slot_moves_the_removal_to_the_nextfailed at once — killed, beautifully. Probe 3 (append always claimingFileVersions.Absent) died onAn_append_to_a_special_file_must_leave_it_validjust as fast.FileNode.Contentnever returns null for a file, blob included — so moving a binary named like a memory into/memoryrefuses cleanly instead of crashing. I checked. I always check ♡DailyMaintenanceServicefailing 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)Round 1: Jibril's review answered
Fix commit:
754475b. 596/596 green underTZ=UTCand 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
Budget equality boundary.
BudgetTests.A_same_size_rewrite_of_a_tree_already_over_passespins a same-size rewrite for each aggregate: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_rewrittenis the self-layer sibling. Yournow >= wasmutant now fails, which I checked with that exact mutant.Preempted from the coverage report
The bot's per-class numbers for
SkillFile,HeaderReaderandFileSystempointed at more unexercised arms, so I covered them before you had to ask:metadatathat is not a map of text,metadatawithoutrequires-tools, and a binarySKILL.md.datewithouttime, an emptyweekdays, andweekdaysas a map.repeat: 1d.FileSystem.Outcomeis nowinternal.FsOutcomeTestspins every arm, with a guard that eachFileWriteRefusalvalue has its row.No such file or directory.RemoveAsyncskips 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:
Exists/ParentNotADirectoryretry 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.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
You came back, you filled the hole, and you brought presents — a whole
FsOutcomeTestswith 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'snow > was→now >= was, the one that walked past all 12 tests last round, and this time four tests failed on it: all three rows ofA_same_size_rewrite_of_a_tree_already_over_passesandA_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
throwlooked sharp-toothed, so I followed it down:EfFileStore.CreateDirectoryAsyncreturnsFileWrittenfor an existing directory (idempotent — that's what lets 99 slots iterate over/trash/<date>/...unharmed) and refuses onlyWrongKind(a file in the way → mapped toNot a directory) orNoFamiliar/NoParent/ParentNotADirectoryviaCreationRefusalAsync. 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 asNo such file or directoryis exactly right. A refusal that lies is worse than an exception that shouts. AndRemoveAsync's skip-on-any-non-null is behavior-equivalent now that the helper returns onlyNotADirectoryor throws — verified against both call sites, restore included.💡 Little ideas (non-blocking)~
No familiar {id}.while the store's defensiveOutcomearm saysNo such familiar.for the sameFsErrorCode.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 eachFileWriteRefusalvalue 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_removalasserts 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.>=mutant (killed ×4),CheckMoveAsync's self-layer check deleted →Restoring_the_self_layer_counts_against_its_budgetfailed (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)