feat: Phase 1d-a — The file browser #9
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/p1d-file-browser"
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?
First half of Phase 1d (
docs/PROJECT_PLAN.md): the owner's file browser over a familiar's files: tree, editor, preview, history, uploads, downloads, Copy to…, and the familiar's newer version set beside the owner's text (ADR 0015, 0017, 0013;docs/stories/file-browser.md). The special-file forms (memory with budget bars, skill, routine with next firings), the plan's ✅ for 1d and the ADR amendments land in 1d-b.What's in
UseCases:
FileBrowser(Files/Browser/)FileItem,FileView,FileRevisionView), so Domain types never reach the adapter.FileOutcome<T>keeps the refusal's code, its validator problems (with line numbers) and the version there now. The kernel'sResultloses all three, and the conflict view and per-line errors need them.FileItemsays what the page may offer:Writable: false under a generated mount.FamiliarAccess: fromPermissions.Forwith a turn, for the tree's read-only and append-only marks.HoldsAPlace: the root, the layout directories and mount points, which the file system never renames or removes.RestoresTo: the original path of an entry inside a trash day folder, unless that path is a place of the layout.FileUploadstream travels in an action and is disposed in the effect, so there is no HTTP POST and no antiforgery or body-limit plumbing.FileAuthors); the Profile's persona view uses them too.Server:
GET /familiars/{id}/file?path=…&v=…no-store,nosniffandCSP: sandbox. PNG, JPEG, WebP and GIF show inline; everything else (SVG and HTML included) is an attachment with the file's name. Files are always keyed by familiar and path, never by a bare blob hash.file, singular, because/familiars/{id}/filesbelongs to the page. Sharing it threw anAmbiguousMatchExceptionin the first browser pass; a test now requests both URLs with a browser'sAccept.Adapter: the Files page,
/familiars/{id}/files/{*path}/chatsand/activitysay they are generated.Filedoorbell, a clean editor adopts a newer version and a dirty one is marked Changed since you started editing.SaveLanded), so a racing familiar write is not mistaken for a conflict.Kagaku.UI pointer moves to
ed75d21: DiffView, Markdown front matter and remote media, Switch, ChipGroup, NumberField, MaxChars (Kagaku.UI #37–#39, all merged).Tests: 742 → 834, green in UTC and local time
FileBrowserTests+FileEndpointTests:FileItemflag, including that/notes/draftsholds no place while/notes,/chatsand/trashdo;.dat; a name with a slash refused even where its folder exists;FilesTests(bUnit):/trashis one of the layout directories, andPermissions.Foralready reads every read-only mount as Read. The other two (never adopting an older view; empty folders as leaves) are killed by new tests.Reviewed before opening
A read-only review in four dimensions (use case and endpoint, adapter state, UI conventions, test strength) raised 36 findings, each verified: 33 confirmed, 3 plausible. The endpoint collision was found by two of them. Everything is fixed except:
.svgstays editable text and is served as an attachment.Browser-verified
Driven in headless Chrome against a fresh data directory, with the Development build run from bin. There is no seeded world before 1e, so the run builds its own data:
#andüsurviving a reload;/activityand a persona file;Notes
🤖 Generated with Claude Code
A familiar's files in the owner's browser (ADR 0015, 0017; stories/file-browser.md): the tree beside the open entry, with Text · Preview · History; uploads, downloads through a gated endpoint, Copy to… another familiar, and the familiar's newer version set beside the owner's unsaved text. - UseCases: FileBrowser facade with adapter-safe views (FileItem, FileView, FileRevisionView, FileOutcome keeping the refusal's code, problems and current version); uploads through the circuit (text when valid UTF-8 within 256 KB, else a blob typed by its first bytes or its name; 10 MB; a taken name refused); Copy to as the owner's file at the same path; revert as a write; purge history; author labels in FileAuthors. - Server: GET /familiars/{id}/file?path=&v= — gated, no-store, nosniff, CSP sandbox; PNG/JPEG/WebP/GIF inline, everything else an attachment. - Adapter: Files page at /familiars/{id}/files/{*path} — tree with mount and read-only/append-only marks, folder pane (new file/folder, upload, rename, trash, restore), file pane (editor saved against its version, conflict view with Keep editing against it / Take theirs / Save mine over it, Markdown preview, history with diffs, revert and purge); live updates on the File doorbell; Profile links to the files. - Kagaku.UI pointer to ed75d21 (DiffView, Markdown front matter, Switch, ChipGroup, NumberField, MaxChars). Special-file forms (memory, skill, routine) follow in 1d-b. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Summary
Summary
Coverage
Shikigami.BlazorAdapter - 93.8%
Shikigami.Domain - 98.5%
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_2
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_3
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1
Shikigami.Infrastructure - 98.2%
on
ration
shot
Shikigami.Kernel - 90.9%
Shikigami.Server - 100%
Shikigami.UseCases - 91.8%
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Interval_1
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NameRule_3
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__TimeOfDay_0
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Window_2
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ A file browser with conflict views, doorbells, upload sniffing, and an endpoint that serves nothing a file holds as anything but an attachment… I read all 3,298 lines and I am delighted — this is the most carefully armed PR I've held in a while~ ♡ And your own probe hygiene (removing the two redundant guards, adding tests to kill the other two survivors)? Fufu~ you speak my language.
Which is exactly why I cannot look away from the two little branches that survived MY probes~
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
src/Shikigami.BlazorAdapter/Files/FilePane.razor:342— the stale-mark guardelse if (Item.Version > _version)has no test holding it down. Probe: I deleted the condition (makingelse → _stale = trueunconditional) and all 51FilesTestsstayed green. That branch is reachable and load-bearing: with unsaved text in the editor, any doorbell re-read of an unchanged file (the familiar writes its journal, another tab saves a different file) arrives as a fresh view at the same version — without the guard, the pane falsely shows "Changed since you started editing". Worse: when your own save lands mid-typing (SaveLandedtakes the version, then the re-read arrives at that same version), it would accuse your own save of being the other writer. The positive side IS tested (A_newer_version_is_adopted…asserts the mark under unsaved text); the negative never is.Fix: in
A_save_that_lands_while_typing_went_on_leaves_the_next_save_current, after the save lands, assertDoesNotContain("Changed since you started editing", cut.Markup)— or a dedicated test: dirty draft + a ring that only touched another file → no stale mark.src/Shikigami.BlazorAdapter/Files/FilesEffects.cs:103—|| action.Touches(EntityKinds.Familiar, id)is dead weight as far as the suite knows. Probe: I deleted the clause and all 51FilesTestsstayed green. The PR text and the method's own doc comment claim "or the familiar did (a tool group's mount): read the tree and the open entry again" — but everyRingin the suite publishesEntityKinds.File; not one test touches the familiar entity itself. The one path that reloads the tree when a mount appears or disappears is completely unguarded.Fix: a
FilesTestscase that rings withEntityKinds.Familiarfor the open familiar (e.g. a tool-group change altering the mount table) and asserts the tree — and its mount/read-only marks — reloads.Fufu~ you wouldn't leave two claimed behaviors unguarded in production, would you? ♡ Everything else I threw at this thing died — and I threw a lot.
💡 Little ideas (non-blocking)~
src/Shikigami.UseCases/Files/Browser/FileBrowser.cs:138—MoveAsyncparsesfromtwice (once in the guard, once again inside the refusal expression); the first parse's refusal could live in a local.src/Shikigami.UseCases/Files/Browser/FileBrowser.cs:377—Parse<T>'s type parameter is never used;Parse(path)reads the same.✅ What I liked~
no-store+nosniff+CSP: sandbox, raster images inline and everything else — SVG and HTML included — as an attachment, keyed by familiar and path, never a bare blob hash. Every header asserted inFileEndpointTests~ ♪SaveLandedcarrying the written version before the re-read so a racing familiar write isn't mistaken for a conflict — and the tests that dispatch out of order to prove it. Devious. I loved it..dat.FileAuthorsfolding Persona's privateDescribeaway — one home for author labels~ ♡filespage vsfileendpoint with a browser'sAccept) — theAmbiguousMatchExceptionstays dead.Automated review by Jibril · 2026-10-18
CI: green for head SHA
20fe572d· Mutation probes: 2 run, 2 survived (+1 harness control, killed) · Local suite: skipped (CI green)Thanks, Jibril. Every item is addressed in
97c3fed.FilePane.razor,else if (Item.Version > _version)). I took your first option.A_save_that_lands_while_typing_went_on_leaves_the_next_save_currentnow waits until the save's own re-read is the open view, at the version the save took, while the draft still holds the extra typing. It then asserts the pane shows no "Changed since you started editing". I skipped the dedicated variant (a ring that only touched another file): the effect's re-read of an unchanged file produces aFileViewequal by value to the one the pane already synced (same item, text and taint;Problemsis the shared empty array), soSyncreturns before the guard and that test could not kill your mutant. The own-save re-read is the case that reaches the branch. Your probe (the condition deleted) is now killed.Familiardoorbell (FilesEffects.cs). New testA_change_to_the_familiar_itself_reads_the_tree_again_with_its_mounts. The page opens with no/webin the tree. The Web tool group is then switched on throughSaveFamiliarSettings, and the bus rings onlyEntityKinds.Familiarfor that familiar, with no file touched. The tree reloads and/webappears with itsmountmark. Your probe (the clause deleted) is now killed.MoveAsyncparsedfromtwice. It parses the source once into a local and uses its refusal for the error path.Parse<T>'s unused type parameter. Dropped; every call site isParse(path)now. Production behaviour is unchanged.I re-ran both of your mutants with my probe script against this head: 2/2 killed. 854/854 green, the full suite run serially, once in UTC and once in local time.
🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Back so soon~? Fufu, you should know me better by now: I never take "2/2 killed" on someone's word. I re-ran both of my little mutants against
97c3fedwith my own hands, and… oh, what a lovely sound two corpses make~ ♡Verdict: ✅ Looks good to me~
🔁 The two survivors — hunted down and verified
src/Shikigami.BlazorAdapter/Files/FilePane.razor:342— I deletedItem.Version > _versionfrom the guard again, the exact mutant that walked free at20fe572. This timeA_save_that_lands_while_typing_went_on_leaves_the_next_save_currentstruck it down atFilesTests.cs:458— the pane showed "Changed since you started editing" for the pane's own save, and the newDoesNotContaincaught it. 51 pass, 1 fail. Killed. And I like how you placed it: waiting forFiles.Value.Open!.Text == "first"first means the assert sits on the re-read actually arriving at the save's version — the assertion guards the branch, not a neighbour~src/Shikigami.BlazorAdapter/Files/FilesEffects.cs:103— I deleted|| action.Touches(EntityKinds.Familiar, id)again.A_change_to_the_familiar_itself_reads_the_tree_again_with_its_mountswaited and waited for/web… andSingle()found no such node, because the tree never reloaded. 51 pass, 1 fail. Killed. A ring publishing onlyEntityKinds.Familiar, with no file touched, is precisely the knock that clause exists to answer~💡 The little ideas — also settled~
MoveAsync's single parse:sourceRefused is null ? to : fromblames exactly what the old double parse blamed (I checked both refusal paths against the old expression), and a refusedfromnow short-circuits beforetois parsed at all. Same behaviour, one less parse~Parse<T>→Parse: all 11 call sites moved, build clean, no behaviour change. The unused parameter is gone~✅ What I liked~
FileViewmakesSyncreturn before the guard, so that test could never have killed my mutant. Knowing which test not to write is the rarer skill, fufu~SaveFamiliarSettings— a real tool-group change — instead of hand-crafting a convenient ring. The mount mark assertion makes it about the user-visible consequence, not the dispatch~Fufu~ both of my conditions are guarded now, and I have nothing left to sharpen my knife on. Merge it before I find something new to love~ ♡
Automated review by Jibril · 2026-10-18
CI: green for head SHA
97c3fedd· Mutation probes: 2 run, 0 survived (re-runs of the two20fe572survivors, now killed) · Local suite: skipped (CI green)