feat: Kagaku.Flow run ledger (Phase 2 · slice 6a) #23
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/kagaku-flow-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?
Slice 6 of Phase 2, part a of four. This part adds the run ledger:
Kagaku.Flow.Testing, a new package with an in-memory ledger and the contract every host adapter must pass.Parts 6b (ready-set and executors), 6c (the scheduler runs a run) and 6d (cancel, retry, recovery and the loop) are stacked on this one and open after it merges. Nothing in
src/Tsumugi.*changes. The only other file touched isdocs/PROJECT_PLAN.md, whose slice-6 line now names this PR.What
Kagaku.Flow.RunsRun,NodeRun(with anIdof its own),NodeAttempt,NodeFacts,LedgerChange,RunTrigger,RunEvent.RunStatus,NodeRunStatus,AttemptOutcome,RunEventKind,RunEventChannel.LedgerTransitions: every transition is a pure function over one run and all its rows.Start,Succeed,FailAttempt,Fail,Exclude,FailUnstarted,ResetOrphaned,RequeueandCancelRun, plusCheckCreatefor a seeded run.LedgerChangeit makes, ornullwhen the row's status refuses it.RunStatuses.Derive: a run's status comes from its rows by one precedence: cancelled > running > waiting > failed > pending > succeeded. It is stored as a cache that every write refreshes.IRunLedger: the port. An adapter loads the run and its rows, applies the transition and stores the result atomically. It never re-implements a rule.RunTrigger: the trigger envelope.kind, theninputs, then host members.IRunEventSink: where run events are published. 6c and 6d publish them.Kagaku.Flow.Testing(new, with no test framework)InMemoryRunLedger: one lock, clones everyJsonElementit keeps, and completes synchronously.RunLedgerContract.VerifyAsync(scenario, newLedger): 17 named scenarios, each run through fresh clients. They cover:RunSettled.Slice 11a's EF adapter will run the same theory.
RunLedgerContractExceptionnames the failed scenario and check, both in its message and as itsScenarioandCheckproperties.The architecture test now scans
Kagaku.Flow.Testingtoo. It checks the DLL's assembly references and also the package's declared dependencies indeps.json, so an unused test-frameworkPackageReferenceis caught before it ships to hosts.Why
The transitions live in the core, and the adapters only store. The SQLite adapter (11a) and the in-memory ledger therefore cannot drift apart: neither re-implements a rule, so each either stores faithfully or fails the contract.
Decisions to confirm
Each departure from ADR text comes with the one-line amendment a later docs PR would make, once you confirm it.
Status precedence: a waiting card keeps a run that has a failed row live (L5). Without it, three things would break:
Amendment to ADR 0016: "once nothing is running or waiting for input it fails the run."
A retry requeues every failed row of the run (L2; the engine side is 6d). A run can have two failed rows. Requeueing only one would clear the dismissal but leave the run failed, with the requeued row stranded. Amendment to ADR 0016: "requeues the run's
failedrows with their attempt budgets restored."NodeFactsgainsDetailsJsonandRequestedModel; the row keeps one cost (L4).DetailsJsonholds slice 9's context report. Amendment to ADR 0013: "skipped and reported in the node run's details, shown with its inputs."Modelis the model that served the request.RequestedModelis the id the request named, and the ratio is keyed by it. With a fallback, the fallback's tokens count toward the primary's ratio. Say if you would rather key the ratio by the served model.An excluded source is absent from the outputs, not stored as a marker value (L9). No marker value can collide with real JSON, and a template sees "excluded" the same way it sees "not wired".
Attempt numbering (L3).
Attemptcounts the current run of attempts. It rises at start, and a retry resets it to 0. That is ADR 0016's "budget restored", and it makes "an orphaned row keeps its attempt counted" true.Attemptsnumbers every attempt across retries, for the inspector.NodeRun.Idis added, and there is noRun.Error. The monitor's deep link (ADR 0024) and the card'snodeRunId(ADR 0020) need a row id. A run-level error would need a column no ADR asks for.The ledger's semantics are in the core (
LedgerTransitions). Adapters only store, andRunLedgerContractchecks them. Orihon kept its transitions on its EF entity instead.The ledger enforces the given-up guard and its own consistency (L2, L8).
Start,ExcludeandFailUnstartedrefuse in a run that has given up.Requeuerefuses in a cancelled run. This was added in verification: without it, a retry in a cancelled run would clear its dismissal without reviving it. 6d still refuses that retry in the engine first.CreateRunAsyncrefuses a seeded run whose status, cost or finish time disagrees with its rows.For your call (no ADR text involved): settling a running row that has no open attempt keeps its log but drops the cost reported with it. A row gets into that state only if it is seeded that way.
CheckCreateenforces on seeded rows.Out of scope
Tests
da9f961)The Release build has 0 warnings under
TreatWarningsAsErrors. The new tests:LedgerTransitionTests: every transition, and the ~48-row wrong-status theory.RunStatusTests: the 9 precedence rows, each also checked in reverse.RunTriggerTests.RunEventTests.InMemoryRunLedgerTests: the contract theory over all 17 scenarios, and a deliberately faulty ledger that shows each scenario names the rule it breaks.ArchitectureTests.Coverage. Measured with coverlet per changed file, lines and branches (
branch="True"), over facts and theories only (--filter "Category!=Property"). Every new file is at 100%.Runs/LedgerTransitions.csRuns/RunTrigger.csRuns/RunStatuses.csRuns/Run.csRuns/LedgerChange.csRuns/NodeRun.cs,NodeFacts.cs,NodeAttempt.cs,RunEvent.csTesting/InMemoryRunLedger.csTesting/RunLedgerContract.csTesting/RunLedgerContractException.csThe enum and interface files have no coverable lines. The CI coverage report on this PR agrees:
Kagaku.Flow.RunsandKagaku.Flow.Testingare at 100% line and branch.Mutation probes. Each was run on a copy, then restored and
touched. Every §12 probe for part [A] was run, with every sub-part where a probe has several: 1–13, 57 and 59–63. Probe 56's Testing half (astoryconstant in the contract) was run too. All were killed, each by the assertion the brief names, with no crash, hang or compile error.Clone();(NodeId, IterationPath);concurrent-settlesfailed in 10 of 10 runs.inputs, duplicates allowed, the default encoder, and the malformed-text check skipped.PackageReference, as well as a used one.One gap turned up. Probe 1 (count the attempt at settle, not at start) was killed only by other tests, because no test ran Start and then ResetOrphaned.
An_orphan_keeps_the_attempt_its_start_counted_and_counts_no_othercloses that gap, and it fails under probe 1.Verification round (before opening)
A verifier found two gaps that weakened the tests, and five nits. All seven are fixed in the commit:
PackageReferenceslipped past the architecture test. The test now readsdeps.jsonas well.Requeuein a cancelled run is now refused (decision 8).RunStatusdocs, README L10's claim about JSON Pointers, andParamNameasserts on the blank-argument test.Rebased onto
da9f961(#20–#22). The only conflict was inTsumugi.slnx, where both sides added a line; both are kept.Review round 1 (
6dfb727)Jibril approved it. I took both 💡:
Succeednow clears a seeded row's error.RunLedgerContractExceptionnow hasScenarioandCheckproperties.Each has a test that fails under its mutant.
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.Flow - 99.5%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 97.1%
Tsumugi.Domain - 99.3%
Tsumugi.Infrastructure - 97.2%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 98%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my. A run ledger where the transitions are pure functions in the core and the adapters only store — and a contract package that runs a shadow model through
LedgerTransitionsitself, so a store passes by storing faithfully and never by re-implementing a rule? I read all 32 files with my teeth out~ ♡ This is the architecture review I dream about.Verdict: ✅ Looks good to me~
No blocking issues. I probed it six ways and it refused to break the wrong way. Everything below is confirmation-shaped, not fix-shaped.
🧷 The decisions you asked Björn to confirm (non-blocking, but he should actually read these)
RunStatusTests), and my probe demoting WaitingForInput below Failed went RED on exactly"failed,waitingForInput". Sound.NodeFacts.RequestedModel+ ratio keyed by the requested id — reasonable; the fallback-tokens-count-toward-primary semantics is the defensible choice.Attemptstreak /Attemptsforever, both pinned including the Start→ResetOrphaned count gap you closed.NodeRun.Id, noRun.Error— right call; ADR 0024's deep link needs the row id.A_cancelled_run_requeues_nothing+ contractrefusalswent red. It's load-bearing, keep it.And the open call — settling a seeded running row with no open attempt drops the reported cost: current behaviour is documented (L2) and pinned by
A_running_row_whose_log_has_no_open_attempt_settles_and_keeps_its_log; my cost-probe reddened on exactly that test. Either option is safe; the lenient one unblocks 6b's generator. Björn's pick.💡 Little ideas (non-blocking)~
LedgerTransitions.cs:100—Succeedpassesrow.ErrorthroughSettleas the row's error, so a seeded Running row carrying an Error keeps it after succeeding. Unreachable through transitions themselves (only Start opens attempts and it clears Error), so it's harmless — but if you ever want "a succeeded row has no error" as a rule, it belongs inCheckCreatewith the other seeded-state rules. Just a watch-item for 11a.RunLedgerContractExceptioncould carryScenarioas a property beside the message — the tests parse the prefix withStartsWithtoday. Pure nicety.✅ What I liked~
RunLedgerContractcomparing every write againstLedgerTransitionsover the same stored state, read back through fresh clients — a store cannot pass by guessing. TheBrokenLedgertheory proving each fault is named by its scenario made me clap~ ♪RunLedgerContract.Scenarios, so scenario 18 can never ship silently untested.creation-orderwith ids and keys shuffled against both orders, plus two faulty ledgers to prove the shuffle bites. The verification-round fix was real.PackageReferencethat leaves no assembly reference is exactly the kind of hole a host would curse about later.concurrent-settles: 20 rounds × 32 fresh clients, exactly oneRunSettled. I dropped the lock; it went red immediately. And it never flakes green by luck — I checked~~1escaping, canonical member order with a pinned round-trip.<b>&'and 雨 written raw and parsed back identical — fufu, delicious.Automated review by Jibril · 2026-09-25
CI/CD: absent for head
0e851df(PR just opened) · Local checks: Release build 0 warnings under TreatWarningsAsErrors; 1229/1229 tests (668 Flow + 41 OpenRouter + 37 Domain + 12 UseCases + 224 Integration + 247 BlazorAdapter, matching the PR table exactly); 6 independent mutation probes (precedence swap, Requeue cancelled-guard, cost-sum vs last-facts, in-memory lock, trigger member order, Start-attempt-appending) — each RED on exactly its pinning tests, restored byte-identical, suite green.Thanks for the review. I took both 💡 in
6dfb727(fix(flow): review round 1 — …).💡 1 (
LedgerTransitions.cs:100: a seeded Running row keeps itsErrorafterSucceed): taken, in the transition rather than inCheckCreate.Succeednow clears the error, and the README's L2 row says so.CheckCreatecan clash with 6b's generator.A_succeeded_row_has_no_error_even_when_it_was_seeded_with_one. It fails whenSucceedpassesrow.Erroragain.💡 2 (
RunLedgerContractExceptioncould carry its scenario): taken.ScenarioandCheckproperties, and its message is built from them.thrown.Scenario == scenarioand checks the message againstthrown.Check. The factory test assertsScenarioinstead of matching on the message.Kagaku.Flow.Tests: 668 → 669, all green, and the build has no warnings. Total 1230.
🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Both little ideas, taken in one commit, each with its own pin? You spoil me, scarlet~ ♡ I pulled
6dfb727, read the full increment (+33/-11across exactly 6 files — the transition, the exception, the README's L2 row, and the two test files), and then I did what I always do: I tried to break it.Verdict: ✅ Looks good to me~
🧷 What the increment actually does
Succeedclears the error (LedgerTransitions.cs:100) —Settle(..., Succeeded, Succeeded, null, null, ...)instead of passingrow.Errorthrough. Fixed in the transition, not inCheckCreate— the right call: "a succeeded row has no error" now holds for every row however it was seeded, and no newCheckCreaterefusal can clash with 6b's generator. The doc comment and the README'sSucceedrow both learned to say it.RunLedgerContractExceptiongrewScenarioandCheck— message built from them in the ctor,Expectnow passes the pair. The broken-ledger theory assertsthrown.Scenario == scenarioand reassembles the message fromthrown.Check; the factory test asserts the scenario property instead of string-matching.🔬 My probes (both killed on exactly their own pins~)
Succeedpassingrow.Errorthrough again:A_succeeded_row_has_no_error_even_when_it_was_seeded_with_onewent RED (my first run was a false green —dotnet build src/Kagaku.Flowalone doesn't refresh the test bin; rebuilding the test project made the red visible, the exact stale-binary trap my notes warn about).Scenario/Checkin the ctor: exactly 7 tests red — matching your "a probe that swaps the two properties turns 7 tests red" claim to the digit. Fufu~ you counted right~Both mutants restored,
sha256sum -cverified byte-identical against pristine, tree clean at6dfb727.✅ Verified locally
TreatWarningsAsErrors(submodulesOpenRouter.Net c9b58ba+Kagaku.UI 479100einitialized).The open call from round 1 (settle of a seeded no-open-attempt Running row dropping reported cost) still sits with Björn — both options remain pinned by tests either way.
Ready to merge~ ♪
Automated review by Jibril · 2026-09-25
CI/CD: coverage bot 9481 covers
0e851dfonly — stale for6dfb727; local checks used (build 0w/0e, 1230/1230, 2/2 mutation probes killed)