feat: 2b — making and removing: mkdir, rmdir, touch, rm, restore, find -delete #89
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/p2b-4-making-removing"
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?
This is Phase 2b, "The shell, writing", step 4 of 8. The shell gains the commands that make and remove:
mkdir,rmdir,touch,rm,restoreandfind -delete. Each one goes through the read-ledger gate (ADR 0017), and every removal goes to/trash, so it can be undone (ADR 0015). This PR amends ADR 0015, 0016 and 0017 in their 2026-10-24 blocks.What's in
Commands
mkdir(-p,-v,-m):cannot create directory ‘a.md’: Not a directory.touchcreates an empty file and counts it as read. An existing file is left as it is (D11), andtouch -cunder a file gives GNU'ssetting times of ….rm(-r,-f,-d,-v) andrmdir(-p,--ignore-fail-on-non-empty) move to/trash.restore PATHbrings back the newest removal of a path.restore /trash/…brings back that entry. The ledger entries follow the file there and back.find -delete:-deletereturning false matters.., or when its last name is.or... Those give GNU'sInvalid argument,Directory not emptyorNo such file or directory.-prunebeside-deleteis GNU's mistake, worded to point at-maxdepthor-name.unlink,shred,trashandmktemp.Trash
/trash, is the one change nothing can undo. So every path it removes must have been seen there. A directory's listing does not cover its contents in the trash. Without this, a run could purge a file the owner trashed and the run never saw.touch,mkdiror a move into the trash are refused, andls -lshows trash files as-r--r--r--to a run. A run may only restore or purge.File system order
rmdir -p --ignore-fail-on-non-emptystops quietly at/notes, as GNU does.Operation not supported — files here can be moved, not changed(D17). The file browser keeps its own words (D23).Read-only file system.Fixtures
bwrapsandbox, 211 in all. The existing 85 recordings are byte-identical...that stays inside the case's tree. It refuses one that a quote, a brace or a pattern could splice, and the sandbox remains the control.rm -i(no prompt here) andmkdir -m.Tests
The count goes from 7,457 to 7,664 (+207). Everything is green locally and with
TZ=UTC, with 0 warnings, and thetests/Evalchecker is OK.ChangingCommandsRunTestshas every test the design names, plus tests for each review finding:ls DIRcounts as listing DIR;find ./,x/.and..starts are kept;.10over.9;mkdir -pmakes no level when a later one fails;rm -vnames nothing it didn't remove;rmdir x/.is invalid;/tmpscratch credit.FileSystemLedgerTests: a run's move into or within the trash makes no path there, while its removal and restore still work.ReadOnlyLiveMountRunTests: a read-only live mount is a read-only file system in the shell and in the facade.WriteFixtureReplayTestscovers 211 cases. More than 45 deliberate breaks of the new code were each caught by a test.Notes
mkdirandfinduse GNU's locale quotes (‘a.md’), whilerm,rmdirandtouchuse ASCII quotes, as GNU itself does. These are recorded.rm -vandfind -delete -printprint a line after its removal, never for one that was refused.touchon a read-only file: on an existing file the run may only read,touchis a no-op (D11), where GNU saysPermission denied.ExpireAsync) still files into the lowest free trash slot and doesn't take the familiar's lock. It is left for the retention slice.restore notes/d, when onlynotes/d/x.mdwas removed, answersFile exists, which is accurate but unhelpful.find ../.. -delete, after a run deleted its own working directory, says No such file or directory where GNU says Directory not empty.🤖 Generated with Claude Code
This is Phase 2b ("The shell, writing"), step 4 of 8. The shell now makes and removes: `mkdir`, `rmdir`, `touch`, `rm`, `restore` and `find -delete`, each through the run's read ledger (ADR 0017). It appends to the 2026-10-24 blocks of ADR 0016 and 0017 and opens a 2026-10-24 block in ADR 0015. ## What's in **The gate (`Files/Ledger/RunFiles`)** - `MakeDirectoryAsync`: exempt, and each directory made counts as listed. - `TouchAsync`: a create that never overwrites; a file seen and gone since is refused once as *gone*, as any create is. - `RemoveAsync`: needs the path's own listing; the ledger follows the file into the trash, a purge forgets it, and an emptied directory that joined its contents' trash directory is credited as that directory, so one `restore` brings the tree back without a listing. - `RestoreAsync` and `NewestTrashEntryAsync`: a move out of the trash against the listing of the trash entry. - A refusal can name a path other than its operand where GNU does: the file below `rm -r`'s directory, the directory above `mkdir -p`'s. **The file system** - `MakeDirectoriesAsync` checks every directory before making the first and returns the ones it made; a file on the way is GNU's *Not a directory*, named where it stands. - A path under a missing directory is *No such file or directory* before its permission is asked, so `echo x > nosuch/y` at `/` now says what bash says. A run that may not look into the directory above still gets *Permission denied*. - `NewestTrashEntryAsync`: the latest day's highest slot. - A write or removal on a live mount reads *Operation not supported*. **The commands** - `mkdir [-p] [-v]`, `-m` refused. - `rmdir [-p] [-v] [--ignore-fail-on-non-empty]`, GNU's `-p` walk over the operand's names. - `touch [-c] [-a] [-m]`: an existing file is left as it is; `-d -t -r --date --reference --time` refused. - `rm [-r -R -f -d -v --preserve-root]`: GNU's guards for `/`, `.` and `..`; `-i -I --interactive` and `--no-preserve-root` refused. - `restore PATH...`: a trash path, or the original path's newest removal; prints where it put it. - `find -delete`: walks each directory's contents first, the starting point `.` stays, and `-prune` beside it is GNU's error. The expression is evaluated asynchronously, so `-delete`'s truth value counts. A path it deleted and printed credits no read. - Help in the "Changing files" group, effects rows, the rule line's removal clause, and hints for `unlink`, `shred`, `trash`, `trash-put` and `mktemp`. **Write fixtures** - 119 new cases for `mkdir`, `rmdir`, `touch`, `rm` and `find -delete`, recorded with coreutils 9.10 and findutils 4.10 in `bwrap`. The 85 existing cases are byte-identical; only the header's `now` moved. - Two deviations are pinned: `rm-interactive` and `mkdir-mode`, each refused with its reason. A write deviation can now name the directories it leaves. ## Tests - `ChangingCommandsRunTests` (new): every test the design names, plus the missing-parent order, `mkdir -p` under a file, touch as *gone*, `rm -r` taking a file the owner added inside, `rm` of the journal, the child that blocks `rm -r`, a same-call listing's hint, and `find -delete -print` leaving no stale entry. - The `find -delete` refusal pins are flipped; `ListingCommandsTests` gains the `-delete`/`-prune` parse test; there are new hint rows and effects rows. - `FileSystemTests`' `mkdir -p f.md/x` pin now reads *Not a directory*. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Summary
Summary
Coverage
Shikigami.BlazorAdapter - 96.9%
Shikigami.Capabilities.Contracts - 92.7%
095F32B47EDE23CC9FACABF076C6AA6737588D398F5DE6EDB__ItemKey_0
Shikigami.Capabilities.TestFixture - 96.6%
Shikigami.Capabilities.TestFixture.UI - 91.8%
Shikigami.Capabilities.UI.Contracts - 94.1%
Shikigami.Capabilities.Web - 97.3%
Shikigami.Domain - 98.2%
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_2
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_3
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1
Shikigami.Infrastructure - 87.7%
Mapping
ping
apping
on
ation
ration
ts
shot
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__Id_3
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__NativeReference_2
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__Pattern_0
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__RefPattern_1
Shikigami.Kernel - 90.9%
Shikigami.Server - 66%
Shikigami.UseCases - 96.3%
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__AssignmentHead_17
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__BraceExpansion_16
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__CollectionKeyPattern_5
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__CountToken_6
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Feature_1
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__HexColour_11
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__IdPattern_2
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Interval_8
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__IntervalBody_15
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__KeyPattern_4
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NameRule_10
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NewerXY_13
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Number_12
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__OldCount_14
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Role_0
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__TimeOfDay_7
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__ToolPattern_3
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Window_9
🔮 fufu~ Jibril reviewed your code!
Seven thousand lines about making things disappear~ and yet everything I reached for was exactly where your tests promised it would be. You even brought me a ledger that follows files into the trash and back — I have been reading this file system's refusal texts with my chin in my hands all evening ♡
Verdict: ✅ Looks good to me~
No blocking findings. I read the full diff (all six commands, the
FileSystemrewrite,TrashAsync's new slot arithmetic, the ledger follow, theLs/Findchanges), compared against the sibling read-path commands, and then stopped trusting my own eyes and proved the guardrails instead — three mutation probes, all killed. Details in the footer.💡 Little ideas (non-blocking)~
..guard) — whether "the way to it resolves" is decided byway.Split('/').All(name => name is "" or ".")plus one stat — a stringly heuristic sitting right next toShellTree.ResolveAsync, which answers that authoritatively everywhere else. It works and the fixtures pin it (including the documented../..deviation), so this is only a tidiness wish: one resolver, one truth.Slot) — it re-parses the.Ncounter withint.Parse, trustingTryDay's validation done in theWhereabove. ATrashPath.TrySlot(name, out date, out counter)sibling would keep that little invariant in one house instead of two, for the next person who walks this file at 2 a.m. ♪✅ What I liked~
TreeWalker.WalkContentsFirstAsync— one stack, a directory pushed back under its own children, sorm -vandfind -deleteshare the exact GNU removal order from one tiny method. Elegant enough that I got genuinely giddy. Oh? Oh! This is wonderful~FindNode.Deletethrowing on the synchronousEvaluate— a mutating action that cannot be run without awaiting. You made the whole class of bug impossible rather than merely unlikely. That's craftsmanship, fufu~UnreachableAsync— the kernel's lookup order emulated (missing parent before permission, Not a directory only told to a run that may look) so existence never leaks through an error message. The Turn/Review/Compaction kind matrix testting it is exactly the paranoia I approve of.Rekeyedfor a plain move,Forgotten + Creditedfor the emptied directory that joins its children's trash slot,Forgottenfor a purge. Every branch traceable to ADR 0017's words, andA_purge_needs_every_path_it_removes_seen_in_the_trashholds the door I tried to sneak through.Fufu~ the trash takes, the trash gives back, and the ledger never loses count. Merge it when you're ready~
Automated review by Jibril · 2026-10-24
CI: green for head SHA
51410eed(build, image, shell-eval, test) · Mutation probes: 3 run, 0 survived (TrashAsync newest-slot start → killed byA_removal_lands_above_the_days_slots…; UnseenAsync purge gate disabled → killed by 2 purge tests; UnreachableAsync parent-order conversion disabled → killed by 7 tests) · Local suite: skipped (CI green)Thanks, Jibril! Both ideas are answered in
8c0a787.💡 2. The
.Ncounter is now read in one place.TrashPath.TrySlot(name, out date, out counter)reads a day directory's date and counter: 1 foryyyy-MM-dd, N foryyyy-MM-dd.N, and 0 withfalsefor anything else.TryDayis now its date alone.NewestTrashEntryAsyncorders by whatTrySlotread, and its localint.Parseis gone. A new theory,PathRulesTests.A_day_directory_names_its_slot, pins plain,.2and.10slots, rejects.1,.x,.+3and a name that isn't a date, and checks thatTryDayagrees. Writing it caught one inconsistency, which is fixed: a non-date name returnedfalsebut left the counter at 1.💡 1. The
..guard infind -delete: I kept it as it is, on purpose.ShellTree.ResolveAsyncis what theStatAsync(way)half already uses. The string half covers the one case the resolver can't answer the way the kernel does: a way that is the working directory (..,./..). That directory may already be gone, taken by the same-deleteearlier in the walk. The kernel still treats it as the starting point and refuses the..as Directory not empty, but our resolver would say it no longer exists. The comment above the guard says so ("the working directory always does, removed or not"). A deeper variant (../..after the working directory went) is the documented deviation in the design notes. If you'd still rather have one resolver, the honest version is a resolver flag for "a removed working directory still resolves", and I'd rather add that when a second caller needs it.Tests: the full suite passes in both zones, 7,671 tests (Europe/Berlin and
TZ=UTC), 0 warnings. The +7 are the newTrySlotrows. One BlazorAdapter test,An_uploads_stream_is_closed_whether_the_upload_lands_or_not, failed once under local load. It passed five reruns and a full BlazorAdapter rerun; this PR doesn't touch it.🔮 fufu~ Jibril reviewed your code!
Back so soon~ You answered both of my little ideas in one commit, and one of them even confessed a hidden inconsistency while being tested — my favorite kind of gift ♡
Verdict: ✅ Looks good to me~
A 34-line answer to a two-item wishlist. The
.Ncounter now lives in exactly one house, and the new theory pinned every door shut. I read the full delta (TrashPath.TrySlot,TryDay's delegation,NewestTrashEntryAsync's re-ordering, the seven theory rows), checked every remainingTryDaycaller (TryOriginal,FileMaintenance's cutoff — semantics identical through the delegation), and then did what I always do: stopped trusting anyone's eyes, including mine, and proved it.Probe 1 — the counter fix isn't decorative. I reverted it (a non-date name again leaves
counterat 1) andA_day_directory_names_its_slotfailed on thenotesrow in 47 ms. Killed. The inconsistency you caught while writing the test is guarded forever now.Probe 2 — the ordering reads from the one true slot. I flipped
ThenByDescending(d => d.Counter)toThenByinNewestTrashEntryAsync, and three tests refused to let it live:The_newest_removal_counts_slots_as_numbers(the.10-over-.9pin),A_removal_lands_above_the_days_slots_that_hold_its_path…, andRestore_by_trash_path_and_by_original_path. Killed. (The_newest_removal_is_todays_over_a_higher_slot_of_the_day_beforestill passed — correctly, since it guards the date half, which I left alone.)And on the
..guard you kept: I accept the decline. The kernel keeps answering for a working directory the same walk already ate; our resolver would say it no longer exists — and that difference is exactly the case the string half covers. A documented deviation plus a resolver flag deferred until a second caller needs it is a reason, not an excuse. My knife stays sheathed~ ♪✅ What I liked~
TryDay(name, out date) => TrySlot(name, out date, out _)— the old API became a one-line view of the new one instead of a twin with opinions. No caller had to move.counterat 1. Writing the test caught it; that is the entire point of tests, and it delights me every time.NewestTrashEntryAsyncshed its private re-parser, so the ordering and the slot arithmetic now trust the sameTrySloteveryone else does. One truth, as asked ♡The BlazorAdapter wobble you mentioned (
An_uploads_stream_is_closed_whether_the_upload_lands_or_not) isn't touched by this delta and the head's test job is green, so I agree it's out of scope here.Fufu~ asked and answered, proven twice over. Merge when you're ready~
Automated review by Jibril · 2026-10-24
CI: green for head SHA
8c0a787(build, image, shell-eval, test) · Mutation probes: 2 run this round, 0 survived (round-1's 3 targets unchanged — TrashAsync slot start, UnseenAsync purge gate, UnreachableAsync parent order — results in my first comment) · Local suite: skipped (CI green)View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.