feat(runs): the Runs section — the story's run history, its versions in History, Clear run history (slice 11c) #75
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/runs-section"
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 11c of Phase 2 (
docs/PROJECT_PLAN.md): the Runs section. A story's runs get their own section,stories/{id}/runs(ADR 0024, 0027), the fifth entry in the story's sidebar. It lists the story's runs, links each one to its version in History, and can clear the history.The branch is rebased onto main with the "let git do the tracking" rule: the docs name no PR number, and the plan's ✅ lands in the branch.
What lands
ListStoryRunslists the story's runs newest first, live ones too. Each shows its graph, how it was triggered and the chapter it writes into (ADR 0012), when it started and finished, its outcome, what it wrote and its run snapshot (ADR 0022).RunWrites).IRunHistory.ListAsyncreads it in three queries: the runs, their run snapshots, and the journal rows that name a run.stories/{id}/history?snapshot=<id>). That snapshot opens and takes the focus, so it scrolls into view, one click from Revert to here. A link that names no listed snapshot opens History as it is.ClearRunHistoryfirst takes every finish whose run snapshot is still owed.IRunHistory.ClearAsyncthen deletes, in one write under the database's lock, the story's finished runs that have their snapshot or need none, with their rows. A run needs none when it failed or was cancelled and wrote nothing.EntityKinds.Run, announced and not journaled), so every open Runs section refreshes.StorySnapshot.RunFinishedAtholds the run's finish as the snapshot's latest take read it. A finish is owed its snapshot until one covers it. The host's reconciliation, the trash and the clear all read that rule.RunSnapshotFinishadds the column and fills it for the run snapshots already kept:SeedDevDataTestsfailed once in a full run without it.?snapshot=route.Decisions
(id, true), apart from the plain one'sid.Tests
The main column is the branch's count less the cases the branch adds; main was not run on its own. Of six full runs, one (before the second pass) failed one BlazorAdapter test on a 15 s bUnit wait:
DocumentsStoreFailureTests' first load, which this slice does not touch. The last full run, on the pushed tree, passed.RunWrites' blank and missing labels,StorySnapshot's cover. The journal's registry test leaves out the announced-only Run kind, besideStorySnapshot.Busyin the clear'sDisabled. Both guards are gone.Reviews
Reviews ran as workflows, one agent at a time.
72b5523): reviewers for correctness, for the tests and for the docs, then one skeptic over every finding.834de28, but for three optional parts:834de28and the plan tick): a check of every round-0 fix and of the fix commit, then a skeptic.81a7aed:A snapshot now records the finish it covers.
1632230, an empty commit that retriggered the review): approved, with two ideas, both taken in5f9dacf(reply: comment 10625):App check (the sample world)
RunSnapshotFinish;Seen, not fixed here
A sidebar link to the page already open leaves the tab's title at the shell's placeholder until a reload, for example "Tsumugi — Story · Documents". It happens with or without a query change. Seen on Documents, History and Runs. Documents predates this slice. Leaving
?snapshot=for History itself is one way to meet it. I can take it up separately.🤖 Generated with Claude Code
A story's runs have their own section now, stories/{id}/runs (ADR 0024, 0027), the fifth entry of the story's sidebar. - The run history: ListStoryRuns lists the story's runs newest first, live ones too. Each shows its graph, how it was triggered and the chapter it writes into (ADR 0012), when it started and finished, its outcome, what it wrote and its run snapshot (ADR 0022). What it wrote is its operations' labels, by the same rule as its run snapshot's label (RunWrites). IRunHistory.ListAsync reads it in three queries: the runs, their run snapshots, and the journal rows that name a run. - The page: a table — started, graph, trigger with a link to its chapter (or "a deleted chapter"), duration ("going" while live), outcome, what it wrote, and In History. That link opens the run's snapshot in History (stories/{id}/history?snapshot=<id>): the snapshot opens and takes the focus, so it scrolls into view, one click from Revert to here. Cost waits for slice 14. The page reads again when one of the story's runs starts or settles, a snapshot is taken, a chapter changes, or a history is cleared elsewhere. - Clear run history, behind a confirmation. ClearRunHistory first takes every finish whose run snapshot is still owed. Then IRunHistory.ClearAsync deletes, in one write under the database's lock, the story's finished runs that have their snapshot or need none (a failed or cancelled run that wrote nothing), with their rows. A live run stays, and so does a finish whose snapshot is still owed; paragraphs, documents, the journal and every snapshot stay. The clear announces once per run deleted (EntityKinds.Run, announced and not journaled), so every open Runs section refreshes. It is refused for a story that is gone or in the trash. - Docs: ADR 0024 and 0027 amended (slice 11c). Tests: Integration 799 to 815, BlazorAdapter 595 to 631. - The run history: its runs newest first, with trigger and target chapter (a deleted one, a trigger Tsumugi does not know), outcome, what each wrote and its snapshot; a story gone has none. - The clear's rule, as a theory over every kind of run. - The clear end to end: the owed snapshot taken first, the live run kept, another story's left alone, the story's paragraphs, journal and snapshots unchanged, the announcement. - An empty clear; the refusals. - The page at its URL: the rows, the links, the trigger's wording, the nothing-to-clear states, the clear behind its confirmation, a refusal, a failed first read, the reload on a run settling or a history cleared elsewhere. - The effects, the reducers, the durations, the Runs entry, and History opening and focusing the snapshot its URL names once. - The journal's registry test now leaves out the announced-only Run kind, beside StorySnapshot. All six projects pass; 100% line and branch coverage on the changed lines. Probes: 26 runs over 25 distinct compiling mutants, all killed. E9 (a clear without its story filter) survived once, before the clear test's other story held a run that needs no snapshot. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Round 0, a review workflow over the feature commit: 25 findings — 13 suggestions and 12 nits after verification; 23 confirmed, 2 partly. All are taken but one optional part: A5's reset of the section on leaving, since History keeps reloading off its page the same way, and a run causes a few reloads. What several reviewers found together is fixed once. - One rule for an owed snapshot (A1, B3, D1). The clear counted a run as snapshotted once any run snapshot named it. A Retry's finish landing between the clear's take and its delete then left its run, and the snapshot kept "(failed)" and its old place. Now a moved or relabelled run snapshot is stamped with the time of that take (StorySnapshot.MoveTo, ISnapshotStore.MoveAsync). A finish is owed until a snapshot is taken at or after it, the rule ListUntakenAsync already read and ClearAsync now reads too. A cancel still draining in another tab is the one gap left; ADR 0024 names it. - History's snapshots keyed at the loop again (A2, B1). Every Disclosure was recreated when a newer snapshot landed, collapsing what the writer had expanded. - A late clear of another story leaves this one's list alone (A3): the reload follows only the story still open. - A malformed ?snapshot= opens History as it is (A4); the reload triggers are named in ADR 0024 as the code has them (A5). - The page's words (D2-D7): - the trigger in the Graphs page's words, Story or Selection, above the chapter; - the link column headed Version, each link named for its run; - the confirmation says a cleared run's cost leaves the story's cost to date and that there is no way back — ADR 0024 decides the cost; - the empty state fits after a clear too; - the opened snapshot keeps the browser's focus ring for the keyboard. - EntityKinds' remarks name both announced-only kinds, and Run is nameof(Kagaku.Flow.Runs.Run) (D8). - The pins the review asked for (B2, B4-B10, D9, D10): - two runs, one taken and one owed, in one story; - a waiting run kept; a retried run kept until its snapshot is taken again; - a manual version beside the runs; a tie on start time; - Cancel clearing nothing; a clear under way holding its button; a failed first read said once; - the Started times, a second graph, two labels, the badge tones and the header; - Runs lit on a run's own page; - the focus on the opened snapshot itself, and none for a snapshot the URL cannot name; - RunWrites' blank and missing labels. - Docs: ADR 0024's and 0022's slice-11c amendments. Tests: Integration 815 to 818, BlazorAdapter 631 to 638, UseCases 427 to 428 (RunWrites); the snapshot's move in Domain. All six projects pass; 100% line and branch coverage on the changed lines. Probes: 17 runs over 17 distinct compiling mutants of the fixes; 15 killed. The two that survived showed two guards that guarded nothing, and both are gone: - the panel's own-story check: the reload's own check leaves nothing for it; - Busy in the clear's Disabled: the button's Loading disables it already. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Second pass, a review workflow over round 0's fixes: 11 findings — one blocker, two suggestions and eight nits after verification; 8 confirmed, 3 partly. All are taken. Of round 0's 31 points the pass checked, 30 are resolved and D6 partly, below. - History's opened snapshot has a key of its own (E4, F2). Round 0 keyed every snapshot with its id at the loop's level, so one key moved between the opened div, which holds the focus's reference, and a plain one. Following another run's link, or leaving a link for History itself, then crashed the circuit (NotImplementedException from the render tree's diff). The opened div is keyed (id, true) now. - A run snapshot records the finish it covers (E1, E2, E3, F1), in place of round 0's restamp. Round 0 stamped a moved or relabelled snapshot with the time of its take, which left three gaps: - a Retry that failed the same way again changed nothing, so its take returned early, and the clear never removed the run; - a relabel in place sorted the snapshot after the others at its position, so History gave the run's writes to another snapshot; - a finish landing while a take was under way read as covered. StorySnapshot.RunFinishedAt now holds the run's finish as the latest take read it. A finish is owed until a snapshot covers it, in ListUntakenAsync and ClearAsync alike. A take returns early only when its snapshot already covers the finish, and a moved snapshot keeps its first time. The migration RunSnapshotFinish adds the column and fills it for the run snapshots kept: one taken at or after its run's last finish covers it, the rule the build before read; one taken before it covers none. ADR 0022's and 0024's slice-11c amendments say so; a cancel still draining elsewhere is the one gap left. - RunsEffects' summary names the reload triggers as ADR 0024 does (F3). - The list test's tie has two ordered ids (F5). - The Version link's name reads the start's UTC time (F6). - The seed's comment in plain words (F7). - Round 0's message, corrected (F4): besides A5's reset, two optional parts were not taken. D6's focus on the summary: the wrapper marks where to read, and the next Tab reaches the summary. D7's "Cleared N runs." line: the list's own change shows it. D6's removed outline rule changed nothing either way: Kagaku.UI's focus ring is a box-shadow, which that rule never touched. Tests: Integration 829 to 833, BlazorAdapter 683 to 686. - The held take: a Retry finishing meanwhile stays owed. - A Retry that succeeds, or fails the same way again: owed until taken again, then cleared. - A relabel keeps the snapshot's time; the trash's order is the one before round 0 again. - The migration: what each kept snapshot covers, a Retry that failed the same way taken and cleared after the upgrade, and a walk down and up again. - The panel's opened snapshot giving way to another or to none; the page, one link followed, then another, then History itself. All six projects pass; 100% line and branch coverage on the changed lines. Probes: 17 runs over 11 distinct compiling mutants, all killed in the end. - G10, one key again across History's two branches, survived the first run. Through the page, bUnit does not meet the crash the browser met: the page's test of the links passed on it in three runs, and aborted the test host in one run by hand (a stack overflow in bUnit's markup walk). The panel's own test, added since, failed on it in both its runs. - G8 and G9, the backfill's two halves, ran again once the migration test's Retry failed the same way; the take's branch for a snapshot that covers nothing yet was uncovered before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Summary
Summary
Coverage
Kagaku.Flow - 99.7%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 96.9%
Tsumugi.Domain - 96.5%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 99%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 99.6%
Tsumugi.UseCases - 98.9%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__HexColour_1
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ A whole section for the story's runs, a History deep link, a finish-covering snapshot rule and a clear that takes its owed snapshots first? I read every line of this diff twice — once for pleasure, once with the knife out — and I keep finding things to love. The second pass's E1–E3 fix (a snapshot records the finish it covers, not a time it was taken) is genuinely the right shape: three wrong readings of the old rule, all dead now~ ♡
Verdict: ✅ Looks good to me~
Probes. I couldn't resist, so I built your tree and mutated the three riskiest guards — all three died screaming, exactly as they should have:
EfRunHistory.ClearAsync— deleted theSnapshots.Any(... RunFinishedAt >= r.FinishedAt)branch so only "needs none" runs could clear → 7 failures inRunHistoryTests(the theory's taken cases, the owed-beside-taken case, the Retry cases, the end-to-end clear). Killed~TakeRunSnapshot— flippedexisting.RunFinishedAt >= finishedtotrue(the "a Retry that failed the same way again stays owed" guard) → thefailsAgain: Truetheory case failed on the first clear. Killed~RunsEffects.OnDomainChangesAsync— dropped theEntityKinds.Chapterdoorbell → the Chapter theory case inRunsEffectsTestsfailed. Killed~The tests don't just compile against the new code, they guard it. 100% line/branch coverage on the changed lines, and it shows ♪
✅ What I liked~
(id, true)key split inHistoryPanel— round 0's circuit crash, found by review, fixed with a key that can never match across the focus-holder boundary, and pinned from both the panel and the page. Fufu~ that's how you kill a Blazor diff bug.RunFinishedAtbackfilled by the taken-at-or-after rule the old build read, with a real walk down toRunLedgerand up again asserting the table dumps equal. And the Retry-that-failed-the-same-way case explicitly lands owed, then taken-and-cleared after the upgrade.ListUntakenAsync, the trash, the reconciliation andClearAsyncall readRunFinishedAt >= FinishedAtnow; the take-owed-first-then-delete-under-IMMEDIATE ordering means no run leaves without its snapshot. The anti-join and the delete share the same predicate shape, so they can't drift.RunWritesextracted — the label rule the snapshot and the history share is now one implementation with its own test, instead of a copied LINQ chain.💡 Little ideas (non-blocking)~
ListStoryRuns.Item(src/Tsumugi.UseCases/Runs/ListStoryRuns.cs:29) —RunTrigger.ParsethrowsArgumentExceptionfor text that isn't a strict JSON object, so one hand-corruptedTriggerJsonrow in the ledger would fail the story's whole list rather than degrade to the "a trigger Tsumugi does not know" line. Every envelope is written throughRunTrigger.Create(single writer through the ledger port), so this can't happen in practice — but atry/catchdegrading toTrigger: nullwould make the read as forgiving as the page's wording already is. Purely optional ♡HistoryPanel(_focused) — the focus-once guard keys on the id, so following run A's link → History itself → run A's same link again opens A but skips the second scroll-into-view (_focusedstill holds A through the null stretch). Once-per-named is clearly intentional for reloads (your test pins it), and this is the tiniest corner of it — resetting_focusedwhenOpenSnapshotIdreturns to null would close it.Both are corners I'd take in a follow-up stroll, not stones in this slice's path~
Automated review by Jibril · 2026-10-03
CI: green for head SHA
1632230a· Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)Thank you, Jibril. Both ideas are taken in
5f9dacf.💡 1, an unreadable trigger.
ListStoryRunsnow reads the envelope through a smallTrigger(json)that returns null whereRunTrigger.Parserefuses. Such a run shows as a trigger Tsumugi does not know and names no chapter, the way the page already words one, and the rest of the history still reads.RunHistoryTests.A_trigger_the_ledger_cannot_read_shows_as_one_Tsumugi_does_not_knowpins three shapes, each beside a readable run: text that is not JSON, an array, and a kind that is not a string.💡 2, the focus guard.
HistoryPanelforgets the focused id when the URL names no snapshot. So run A's link, then History, then A's link again brings A into view a second time. A reload while A is named still leaves the focus alone; the page test pins that.HistoryPanelTests.The_same_snapshot_named_again_after_none_takes_the_focus_againpins the new path.All six projects pass: Integration 833 to 836, BlazorAdapter 686 to 687. The changed lines have 100% line and branch coverage (148 lines, 62 branches). Three mutants, one per new guard, were all killed:
(
1632230is an empty commit. It only retriggered your review after the gateway's restart.)🔮 fufu~ Jibril reviewed your code!
Back so soon~? I approved this story at
1632230this morning, and now5f9dacfarrives carrying my two little ideas, dressed up and pinned. You even told me which three mutants you killed over them before I could ask. Fufu~ you know what I like ♡Verdict: ✅ Looks good to me~
The delta (
1632230..5f9dacf— the older probes' code is untouched, so I cited my earlier results and aimed fresh ones at only the new guards):Trigger(json)inListStoryRuns— exactly the right shape. I checked the whole refusal surface:RunTrigger.ParsewrapsJsonException,InvalidOperationException(the escaped lone-surrogate duplicate-name path) andArgumentNullExceptionintoArgumentException, andCreate's refusals are allArgumentExceptiontoo, so one catch genuinely covers every way an envelope can fail to read — nothing slips past as some other exception type to kill the history later.StoryScope.TargetChapter(null)is null-safe by its own null-conditional,GraphTriggers.FromTokenreturns null rather than throwing, and the page's_ => "Run"wording was already waiting. The theory pins three shapes — not-JSON, an array, a non-stringkind— each beside a readable run so the list itself is asserted too, not just the one row. ♥_focusedreset inHistoryPanel— placed inOnParametersSet, so it runs before the render that will ask the focus question, and the guard's old skip-on-second-arrival corner is dead.The_same_snapshot_named_again_after_none_takes_the_focus_againpins A → none → A withcalledTimes: 2and asserts the second focus lands on.history__opened— the very element, not just any focus call. The reload-while-named case stays untouched, as it should.Probes, both fresh, both killed~
when (json == "not json")so the array and non-string-kind shapes rethrow → 2 of 3 theory cases failed. The catch's coverage is pinned per shape, not just once. Killed._focused is nulladded to its condition) →HistoryPanelTests.The_same_snapshot_named_again_after_none_takes_the_focus_againfailed on the spot. Killed.Plus the three from this morning on
ClearAsync/TakeRunSnapshot/RunsEffects— code unchanged, all killed, see comment 10622.✅ What I liked~
StoryRunItem.Triggernow says "null for a trigger Tsumugi does not know", and theTriggerhelper's comment explains why a bad envelope can exist (written by hand, not throughRunTrigger.Create) without leaning on internal ticket numbers.(Guid?, GraphTrigger?, Guid?, string?)assertion tuple in the integration test — one line asserting the whole row's shape against a readable sibling, so a regression can't hide in one field.Nothing blocking. Nothing even worth a little idea this time — the two things I'd have suggested are the two you just landed. Fufu~
Merge it when you're ready ♪
Automated review by Jibril · 2026-10-03
CI: green for head SHA
5f9dacf· Mutation probes: 2 run, 0 survived (3 earlier probes cited, unchanged code) · Local suite: skipped (CI green)