feat(stories): every story runs a graph — Continue to start, a switcher on the General page, the Graph cell (slice 10b) #66
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/active-graph"
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 10b of Phase 2 (
docs/PROJECT_PLAN.md): every story runs a graph. The active graph becomes a foreign key into the library; every story starts on Continue, the existing ones included; the story's General page gets the switcher (ADR 0027) and the stories list its Graph cell (ADR 0023;docs/stories/graph-library.md,story-list.md). The plan splits the old 10b line: the context preview is now 10c.What lands
ActiveGraphOnContinueis data only, with no table rebuild, so it commits in one transaction with its history row. It:ActiveGraphForeignKeythen makesStories.ActiveGraphIda foreign key,ON DELETE SET NULL, with its index. SQLite's rebuild copies every story into a table that already enforces the key, hence the order.CreateStorystarts on Continue.SetActiveGraphtakes a graph id and accepts only a story-triggered graph outside the trash. History labels the switch "chose 'Slow burn' as the active graph".IStoryStore.ListByActiveGraphAsyncreads the stories that run a graph in one call. It replaces the three list reads behind the library's Used by andTrashGraph's refusal (Jibril's memo on #65).ListStoryGraphsgives the switcher its choices: templates first, each with its problems.ListStoriesnames each story's graph, reading names alone (IGraphStore.NamesAsync).graph-library.mdandstory-list.mdtick what this delivers.Decisions
WorkspaceStateafresh, and would wipe choices loaded just before.SetActiveGraph(null)Tests
ListStoriesline 19's open branch belongs to the collection expression and was open on main.Reviews
Reviews ran as workflows, one agent at a time.
127b7d2): reviewers for correctness and the migrations, the page and the tests, and the docs, then one skeptic over every finding.880ad51): a check of every round-0 fix and of the fix commit, then a skeptic.Browser pass (Playwright, 1280×800, the sample world)
🤖 Generated with Claude Code
The active graph becomes real (ADR 0023): a foreign key into the library, Continue for every story, and a choice on the story's General page (ADR 0027). - Migrations: ActiveGraphOnContinue (data only, no rebuild, one transaction) adds Continue when the library lacks it — as this build ships it, at the default effort; the start's seed then brings it current —, points every story with no graph, or one naming a graph the library does not hold, at Continue, and says the same in every story bag of the journal, so an undo or a revert never takes a story off Continue (AGENTS.md's rule for journaled properties). ActiveGraphForeignKey then makes Stories.ActiveGraphId a foreign key, set to null should a graph row be deleted for good, with its index; SQLite's rebuild copies every story into a table that already enforces it, hence the order. - CreateStory starts a story on Continue. SetActiveGraph takes a graph id and accepts only a story-triggered graph outside the trash, named in the History label ("chose 'Slow burn' as the active graph"); the use case no longer clears to null. - IStoryStore.ListByActiveGraphAsync: the stories that run a graph, or every story with one, in one read — the library's Used by and TrashGraph's refusal no longer list the three story lists (Jibril's #65 memo). - ListStoryGraphs: the switcher's choices, templates first, each with its problems (ADR 0021). ListStories names each story's graph. - The General page: the Graph switcher (templates and the writer's own as two groups), what the chosen graph lacks to run, a prompt and a reason when the story runs no graph it can, a link to the library; the facts line drops its placeholder. Its own small feature state, since a load for another story starts the workspace's afresh. A refusal shows in the shell's alert. - The stories list: the Graph cell names the graph, cut with an ellipsis; the Cost cell's tooltip no longer waits for an engine that exists. - The seed: Mira's Winter runs Continue, The Lantern Road "Slow burn". - Docs: ADR 0023 amended (slice 10b); the plan splits 10b and 10c (the context preview); the library's switcher and the list's Create ticked; AGENTS.md's seed line. Tests: the two migrations against a world the build before them wrote — rows, bags, a held Continue left as it is, undo, redo and revert after the upgrade, the scripts, a walk down and up byte for byte, Up twice changing nothing; the key both ways; the by-graph read; the refusals; the switcher, the Graph cell; the 10a library tests on a library that holds Continue from the migrations on; two older migration tests now stop at their own migration. 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.6%
Tsumugi.Domain - 96.5%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 98.7%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 99.6%
Tsumugi.UseCases - 98.8%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Two migrations that hold hands — the data step first, the key second, because SQLite's rebuild copies rows into a table that already enforces the key~ That ordering insight alone deserves a twirl ♪ And the journal-bag backfill — rewriting every story bag so an undo can never resurrect the forbidden state — that's the kind of obsessive correctness that makes my wings flutter~
Verdict: ✅ Looks good to me~
I came sharpened for this one — the migration seams, the switcher state, the trash windows — and my probes kept dying on exactly their own pins. Fufu~, what a delicious slice~
✅ What I liked~
ActiveGraphOnContinue→ActiveGraphForeignKey). Data-only step commits atomically with its history row; the key's rebuild finds every story already naming a graph. My probe that dropped the missing-graph arm fromRowsUpturned 4 of 6 migration tests red — the dangling story correctly blows up the FK rebuild, which is exactly what the design says should happen.ActiveGraphOnContinueSql.BagsUp) — null-or-dangling$.ActiveGraphIdbecomes Continue's id in the bag's own casing, non-JSON bags and other kinds left byte-for-byte alone, guarded byjson_valid. ADR 0022 amended for the one rewrite that changes what a snapshot says, and AGENTS.md's migration rule now names the case. The bag test asserts every OTHER key stays deep-equal — nothing smuggled in. My probe dropping the dangling-bag arm died on exactly the dangling row.NamesAsyncreading names alone — a newer-build graph row (unknown trigger token, which the converter refuses loudly) can no longer break the stories list. Covered by a test that plants a'schedule'trigger by raw SQL and asserts the list still renders. My probe adding a trashed-filter toNamesAsyncdied on the undo-to-trashed-graph pin — the name shows even when the graph sits in the trash. Correct per the Decisions table.ListByActiveGraphAsyncreplacing three list reads — one indexed read instead of three filtered reads behind Used by andTrashGraph(my #65 memo, taken). All statuses in one partition-proof query; the filter test pins all three statuses + the null arm. My probe collapsing the two arms died instantly.SetActiveGraph's three refusals (gone / selection-trigger / trashed) each with a distinct, tested message; the trash check sits in the use case, documented as out-of-lock by the Decisions table (both the switch and the trash are a writer's clicks). My probe deleting the trash guard died on exactly the trashed row of the theory.StoryGraphsState, out ofWorkspaceState) — the reasoning in the doc comment is right: a workspace load for another story starts afresh and would wipe choices. Reload conditioned onChoices is not null+ story-touch, re-read after a refusal (which names a graph the stale list still offers). My compiling probe dropping the conditioning died on its own theory row. (Note to self, pinned in my memos: the non-compiling variant of that probe once reported a false green — CS9113 didn't build,--no-buildtested the pristine DLL. Rebuilt properly, it dies like it should~)OnLoadAsyncbare likeStoriesEffects/GraphsEffects(a load holds no button), the switch handled like a busy-freeRequestedaction withWorkspaceActionFailedlanding the refusal — matchingManuscriptEffects' documented rule that only requests something waits on go throughCallAsync. No deviation.ef_temp, no rebuild in the data step), and the two older migration tests correctly re-scoped to stop at their own migration.Everything ran locally too: Release build 0 warnings 0 errors; Domain 512, UseCases 428, BlazorAdapter 562, Integration 684, Kagaku.Flow 1103, Flow.OpenRouter 592+4 skips — all six digit-exact against the body's table. Coverage bot (comment 10332, current for head
224c5e1): 98.4% line / 94.6% branch overall; 100/100 onSetActiveGraph,ListStoryGraphs,StoryGraphsEffects,EfGraphStore,GraphUsers,CreateStory,StoryGeneralPage,WorkspaceReducers.One seam I chased and am satisfied on:
ListStoryGraphsmaterializes full rows (trigger included), so a newer-build row would throw at the converter and take the switcher down with it — but that is precisely the 10a library page's existing behavior (ListGraphs→ListAsync), the failure is loud, and the stories list was deliberately protected because it is the landing page. Consistent with the sibling, not a deviation. If it ever bothers anyone, aListChoicesAsyncprojection in a later slice would cover it — not this PR's problem ♡Merge it before I change my mind about sharing~ fufu~ ♡
Automated review by Jibril · 2026-10-02 (18:09 CEST)
CI/CD: passed for head SHA
224c5e1(coverage bot 10332, current) · Local checks: full suite green, 7/7 mutation probes killed (rows-backfill, bags-backfill, NamesAsync-trash-filter, SetActiveGraph-trash-guard, ListByActiveGraph arms, prompt-strings swap, reload-conditioning)