feat: 2b — the read ledger #73
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/p2b-2-read-ledger"
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 2 of 8. It adds the read ledger from ADR 0017. A run, or the chat it belongs to, records which version of each path it was shown, both the file's content and its directory listing. The file system refuses a change composed from something the run never saw, or saw at an older version. The store's own version check backs this up and gives the same message. The shell still refuses writes; PR 3 turns
>/>>/teeon over this. Amends ADR 0017 and ADR 0022.What's in
Domain
LedgerScope: a chat, or a run without a chat.ReadLedgerEntry: one row per path. It keeps the content version and the listing version apart, the stored row seen there, the/tmpscratch mark, and when the path was last read.Use cases (
Files/Ledger/)RunScopeholds the reads waiting to be credited, a sequence counter, the run's own last change per path, and the untrusted mark.ReadLedger.CreditSeenAsynccredits waiting reads in sequence order, in one transaction. It drops a read older than the run's own change of that path or of a directory above it.ApplyOwnAsyncstamps the run's own change before persisting it, so a failure fails closed.ReadLedgerRules.Admitapplies D4's step-6 order:/tmp;ConflictReportwrites every D17 message form. It names the author, the time in the app's zone and a capped diff, says where a file that is gone went, and adds the "a read counts once you have seen it" hint only when a waiting read would cure the refusal.FileSystemExpectation.Seenis wired into write, text write, the redirection pre-check, append, move (stored and live), remove, purge and restore.Shell
lsentry or a step offind,tree,grep -rorrg.sed s,tr,cut) credits nothing.cat a.md/xnow says GNU's "Not a directory".Infrastructure
EfReadLedgerStore: every batch runs insideWriteAsync. A path and everything below it are found by the same byte range the file store uses.ReadLedgermigration, with a unique index on (familiar, scope, path) and cascade delete with the familiar.Seed
cat /notes/tracking.mdthrough the real shell and leaves a ledger row. The browser-test skill mentions it.Tests
The count goes from 6,510 to 6,745 (+235). The suite is green locally and with
TZ=UTC, with 0 warnings.ReadLedgerEntryTests,ReadLedgerRulesTestsandReadLedgerTestspin each admission branch in order, the exact refusal text, overtaken reads, a failed credit keeping its reads waiting, and reads queued mid-credit surviving.ConflictReportTests(fake source) pins every D17 form, plus:(ls 'my notes').FileSystemLedgerTests:/tmpsource leaving the file it would replace in place;ConflictReportfrom DI for changed, moved, removed, another file and live cases.ReadLedgerStoreTests(16): rekeys, byte ranges, root forget, rollback of a partly failed batch with no announcements, and eight concurrent credits of one new path that never collide.LedgerCreditRunTests:RunMarkTestscheck that the mark is set by the read itself while its command runs. A test-only probe command proves this.Notes
ConflictReporthas no production caller until PR 3's run-side writes. Today it is only registered in DI and exercised by the tests above.grep -rwith no match andsort < mail. This is wider than the two calls the design listed, and it follows ADR 0022's "whether or not it reached the model".wc,head,stat,file), a path only a glob named credits nothing. Their refusal names the directory to list.🤖 Generated with Claude Code
Summary
Summary
Coverage
Shikigami.BlazorAdapter - 96.6%
Shikigami.Capabilities.Contracts - 100%
Shikigami.Capabilities.TestFixture - 95.6%
Shikigami.Capabilities.TestFixture.UI - 100%
Shikigami.Capabilities.UI.Contracts - 91.3%
Shikigami.Capabilities.Web - 97.1%
Shikigami.Domain - 98.2%
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_2
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_3
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1
Shikigami.Infrastructure - 95.1%
Mapping
ping
apping
on
ation
ration
ts
shot
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__Id_1
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__Pattern_0
Shikigami.Kernel - 90.9%
Shikigami.Server - 86.4%
Shikigami.UseCases - 96.3%
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__AssignmentHead_13
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__BraceExpansion_12
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__HexColour_7
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__IdPattern_0
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Interval_4
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__IntervalBody_11
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__KeyPattern_2
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NameRule_6
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NewerXY_9
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Number_8
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__OldCount_10
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__TimeOfDay_3
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__ToolPattern_1
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Window_5
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A read ledger with content and listing versions kept apart, a name-coverage algorithm that refuses to credit a fragment shown twice, a store backstop that speaks the ledger's words, and a seed that walks its own shell… I read all 64 files of this diff with my wings folded and my eyes very wide~ ♡ This is the kind of PR I want to keep in a glass case. And then I poked it with mutations, because love means verifying~
Verdict: ✅ Looks good to me~
🔬 Mutation probes (3 run, 0 survived)
Terminal.VisibleSources— replaced the contiguous-name-coverage check with "any visible fragment credits". 9 tests failed (A_walk_listing_whose_names_were_rewritten_or_never_printed_credits_nothing, the repeated-fragment theories, glob-unshown). TheOffset-based interval coverage has real teeth —grep -o '^a' | sed 'p;p;p'cannot smuggle a name through~RunScope.Overtaken— removed the parent-directory walk (exact path only). BothA_read_before_an_own_change_of_its_path_or_a_directory_above_is_dropped(unit) andA_read_of_a_path_the_run_then_moved_or_removed_is_not_credited(integration) failed. A read under a moved/removed directory can not be resurrected as ghost knowledge~ReadLedgerRules.Admit— deleted the another-row-at-path refusal. 3 tests failed (Another_row_at_the_path_is_gone, its/tmpsibling,The_report_tells_of_another_file_at_the_path). A file replaced at the same path cannot pass as the one the run saw. Good~ because if that mutant had survived, we would have had words, you and I~ ♡✅ What I liked~
ReadLedgerRules.Admit's D4 step order is pinned exactly where the ADR says it sits — the permission/mount/layout/kind-before, validator/budgets-after tests (Permission_append_only_and_unsupported_mounts_are_reported_before_not_read,Not_read_is_reported_before_the_validator_and_the_budget) are the refusal-order proof I always hope for and rarely get~RaceStoredispatch-proxy tests prove the backstop answers in the ledger's words for every mutator, including the trash-step race. Two writes from one read land exactly once. Fufu, delightful~ApplyOwnAsyncstampsrun.Changedbefore persisting, so even a failed persist keeps later reads overtaken — andAn_own_change_is_stamped_even_when_it_fails_to_persistproves it.EfReadLedgerStore: parameterizedSqlQuerywith the same/-to-0byte-range trick the file store uses, unique index on (FamiliarId, ScopeKind, ScopeId, Path), cascade with the familiar,Purging_a_familiar_deletes_its_ledgers. The migration matches the snapshot and configuration exactly.ConflictReport— Berlin-midnight dates, a revision pastCurrentcorrectly ignored, surrogate pairs never split at the 200-char cut,(ls 'my notes')quoted. D17 is fully spoken~Consumedmarking the run at the moment of read (evengrep -rwith no match,sort < mail) while pure listings mark nothing — theProbecommand proving the mark lands mid-command is a genuinely clever test.💡 Little ideas (non-blocking)~
FileSystem.cs:217-240— when the expectation is ledgered,CheckWriteAsyncfetches the row twice (existingup top forcreates, thencurrentagain for kind/version). Since the pre-check holds no lock anyway, the second fetch could reuse the first (current = ledgered is null ? await files.GetAsync(...) : existing) — one store read fewer per redirection open.Executor.cs:195-197— with stdout nulled/closed, the hook's immediate record drops found reads even if a command showed those names on stderr; the finalVisibleRecordsstill credits them correctly (it reads the terminal), so this only affects the hook's view. No catalog command writes found names to stderr today — just noting the seam for whoever adds one.The
ConflictReport-has-no-production-caller-yet note matches the PR description (PR 3 wires run-side writes), and holding back the one false ADR sentence was the right call — writing it now would be a lie in the docs, and we can't have that~ ♪Automated review by Jibril · 2026-10-24
CI: green for head SHA
cd43e10e(build/image/shell-eval/test all success) · Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)Thanks, Jibril! Both ideas are addressed in
b90b1fa.💡 1. Double fetch in
CheckWriteAsync. When the expectation is ledgered, the pre-check now reuses the row it already looked up (current = ledgered is null ? await files.GetAsync(...) : existing), so a redirection open makes one store read fewer. A comment says why this is sound: the pre-check holds no lock, and the write checks everything again under it.💡 2. Hook view with stdout discarded. I've written the seam down at the spot in
Executor.RecordAsyncwhere it lives. With standard output discarded, the hook sees no found path, because no command shows found names on stderr today. A command that does must classify them there, or the hook will miss what the finalVisibleRecordscredits. I didn't change the behaviour: the hook can't tell from the descriptors which stream carried a name, and widening it now would let found reads reach the hook for output nobody saw.No tests were added: both changes are a refactor and a comment, so behaviour is unchanged. The full suite passes in both zones: 6,745 tests (Europe/Berlin and
TZ=UTC), 0 warnings.🔮 fufu~ Jibril reviewed your code!
Oh? You came back with exactly five lines — a row reused, a seam written down — and neither of them flinched~ ♡ I re-read the delta (
cd43e10e..b90b1fa), the whole ofCheckWriteAsyncaround it, andRecordAsyncwhere the new words live. Then, because a refactor that claims to change nothing is exactly the kind of claim I like to test, I mutated the reused row tonulland let the suite speak~Verdict: ✅ Looks good to me~
🔬 Mutation probe (1 run, 0 survived)
FileSystem.cs:235— replaced the ledgered branch ofcurrent = ledgered is null ? await files.GetAsync(...) : existingwith: null, so a ledgered pre-check would always believe no row exists. 2 tests failed:A_checked_append_needs_the_content_read_and_the_journal_none(theNotReadrefusal on an existing, unread row needs the real row) andA_directory_in_the_way_is_its_kind_before_anything_the_ledger_says(kind-before-ledger needs to see the directory). The reuse is load-bearing and guarded~ ♪The three probes from round 1 (
Terminal.VisibleSources,RunScope.Overtaken,ReadLedgerRules.Admit) sit in code this commit doesn't touch — not re-run, still cited as killed.✅ What I liked~
existing(line 218) and the fetch it replaces took identical arguments inside the same lock-free window, and the comment says why that is sound — the pre-check holds no lock and the write checks everything again under it. A mechanics comment, exactly where it belongs~RecordAsyncwas the correct call. The hook cannot tell from descriptors which stream carried a name; inventing stderr-found classification for a command that doesn't exist yet would credit output nobody saw. Writing the seam down at the spot, with the condition a future command must meet — that's how you leave a trap that catches the right person~ ♡Fufu~ both little ideas addressed, five lines, zero ceremony. This is what a review response should look like~
Automated review by Jibril · 2026-10-24
CI: green for head SHA
b90b1faa(build/image/shell-eval/test all success) · Mutation probes: 1 run, 0 survived / 3 skipped (unchanged since round 1) · Local suite: skipped (CI green)