feat(commit): the commit node lands a run's prose, and the playground reads the registry (slice 9c) #64
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/commit-node"
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 slice 9c of Phase 2, the last of three PRs for "the story half":
story.*and the document rendering.contextnode.commitnode, and the registry in the playground.The
commitnode lands a run's prose in the manuscript. It reads the model's text with the lenient reader and appends it to the run's target chapter as one journaled operation of the run. Each paragraph is credited to the node and model that wrote it. The playground's canvas now reads its node types from the registry, through a use case, and its descriptor literal is gone.Commits (rebased on main
9324103, #63):4817dbd: the node, its write path, the registry in the playground, tests and docs.bb883bf: the review's round 0. It puts the chapter delete, merge and move under the write lock, decides the fallback chapter there, and makes the commit land once per run.4f227c0: the second pass. A commit lands once per run whatever became of that landing, and a Retry answers in reading order.8054fa1: this PR's number in ADR 0012, ADR 0015 and the plan.376596e: Jibril's 💡, the store refuses a merge of a chapter into itself.What
Tsumugi.UseCases/Commit:CommitConfighas two settings, both optional, so{}is a valid config:mode:append. It is the one mode until thetargetsink and the target chapter setting arrive in Phase 3 with replace and the inserts.split: true by default, so each blank line starts a new paragraph. Set to false, the whole text lands as one segment, its paragraphs a blank line apart.Every problem is reported at its JSON pointer, and an unknown key is refused.
CommitDescriptoris the playground's canvas entry: group Output, colour--warning, a requiredtextsink and asegmentsjson source. It has one attempt, as ADR 0016 gives every type butllmandcontext, and it takes no slot of the LLM limit. Its config schema agrees withValidateConfig.CommitExecutor:ModelOutput(ADR 0030): emphasis is read, a model's5 * 3 * 2stays literal, and a paragraph that is only***or* * *becomes a scene break;Provenance.FromRun(run, upstream node, model)from the upstream row oftext, where the model is the one the provider served, else the one the node asked for;segmentsas{chapterId, segmentIds, text}: the chapter, the segments in reading order, and their text as a template reads story text;earlier).AppendRunTextis the commit's write path, and the seed uses it too:The stores:
ISegmentStore.AppendAsyncdecides everything under the database's write lock (ADR 0028):It returns the chapter it landed in.
IChapterStore.DeleteAsyncreads the chapter's segments under the lock, so a paragraph a commit lands just as the delete begins is deleted with a journal row, and Undo brings it back.The new
IChapterStore.MergeAsyncmoves the source's segments and deletes the source in one locked save;MergeChapterscalls it. It refuses a merge of a chapter into itself.MoveToChapterAsyncreads the target's last place under the lock.The playground gets the canvas JSON from a new
GetNodeTypesuse case, so the adapter never names an engine type (ADR 0003).AddInfrastructureregisters the engine core (AddKagakuFlow), whose registry collectscontext,commitandllm. The engine itself resolves once 11a registers its ledger and event sink.SampleGraph.DescriptorsJsonis deleted. The engine tests' copy of it (sample-descriptors.json) stays, andPlaygroundRegistryTestsnow checks it against the real registry.The seed lands its run paragraphs through
AppendRunText. History shows the commit's operation ("Commit appended 2 paragraphs", nodecommit), and the paragraphs stay credited towriteronseed/model.Docs:
AppendRunTextas the commit's run-write path.Why
commitnode, its sockets and config, provenance from the upstream row, one journaled operation, andsegmentsfor a follow-up node.Decisions to confirm
segmentsis{chapterId, segmentIds, text}. The ids are the refs a Phase 3targetwill take. The text lets a follow-up node read what landed in a template ({{ inputs.segments.text }}) without a tool.openrouter/autowould be credited instead of the model that wrote the prose.story.*and the context node already read it. A commit decides this under the write lock, so a delete that lands at that moment is seen. The target is current again once Undo restores it. The prose continues where the story now ends rather than failing the run.llmandcontextone attempt. A crash or a lost settle can orphan an attempt whose write landed; its Retry then writes nothing, whatever became of that landing. Revert run followed by Redo run is how reverted text comes back, so a Retry never duplicates prose. Phase 3'smapandloopwill key this on the iteration too.split: falsejoins the paragraphs a blank line apart inside one segment, a scene break kept as its***text. A lone paragraph, including a lone scene break, stays as it reads.commit, where it read "Writer committed 2 paragraphs" underwriter. 11b adds the seeded ledger runs.Out of scope
replace,insertAfter,insertBefore, thetargetsink and the target chapter setting (Phase 3).Tests
Every project was run one at a time, after a full build including the script modules, on main
9324103:CommitNodeTests29, the store 11, the registry against the engine fixture 1)All pass.
CommitNodeTestsruns the engine over the real stores. Beyond landing, provenance, output, report, label and failures, it covers:A new test helper,
WriteHook, runs a write just as another write's database transaction begins. The store tests use it to race each locked path against a write landing in between: the append's place, the fallback, the delete, the merge (from the source side and the target side), and the move.Coverage
Coverlet with branch coverage, on the use-case, integration and adapter tests: 100% of lines and branches on every line this PR adds or changes.
Commit/AppendRunText.csCommit/CommitConfig.csCommit/CommitDescriptor.csCommit/CommitExecutor.csGraphs/GetNodeTypes.csChapters/MergeChapters.csStores/EfSegmentStore.csStores/EfChapterStore.csDependencyInjection.cs(UseCases, Infrastructure)Graphs/GraphPlaygroundPage.razorDiagnostics/SeedDevData.cs¹ The run-id guard's two arms are covered by two projects: the unit test takes the refusal and the integration tests the pass. The merge tool reports the higher of the two per line, so it shows 13/14.
² Coverlet reports no branch data inside
WriteAsynclambdas in this file. The two operands of the merge's "chapter gone" check each have a test.³ These gaps predate this PR: the seed's refusal arms, which throw only if the seed itself is wrong.
Mutation probes
Each probe is a single edit (or a lock swapped for a no-lock helper), built and tested on its own, one at a time.
Reviews
Each round was a review workflow followed by a skeptic for each dimension, one agent at a time on one shared copy of the branch.
bb883bf, plus D9's optional note. bb883bf's message says 14 confirmed and 7 partly, which is wrong; the right split is 16 and 5. The main findings:4f227c0:Browser pass
GetNodeTypes.🤖 Generated with Claude Code
The commit node (ADR 0015) reads its text with the lenient reader (ADR 0030) and appends it to the run's target chapter as one journaled operation of the run (ADR 0022), named by the node: "Commit appended 2 paragraphs and a scene break". Each segment is credited to the upstream node of its text sink and the model that row reports served, else the one it asked for (ADR 0012). Its segments output is {chapterId, segmentIds, text}; its report names the chapter and whom it credited. Config: mode (append, the one mode until the target sink arrives in Phase 3) and split (default on). One attempt, no slot. An empty text fails the row and writes nothing. - AppendRunText is the commit's write path, shared with the seed, whose run paragraphs now land as the commit's operation. - ISegmentStore.AppendAsync places the new segments after the chapter's last one under the write lock, so a paragraph typed meanwhile never shares a place. - AddInfrastructure registers the engine core; the playground hands the canvas NodeRegistry.ToCanvasJson() — context, commit and llm — and SampleGraph.DescriptorsJson is deleted. The engine tests' copy of those entries is now held against the real registry (PlaygroundRegistryTests). - ADR 0015 and 0012 amended: the Phase 2 config, the segments shape, the model credited, the label, one attempt, and a deleted target chapter falling back to the last for every read and commit alike. AGENTS.md names AppendRunText. 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.5%
Tsumugi.Domain - 96.3%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 98.4%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 98.6%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
🔮 fufu~ Jibril reviewed your code!
The last of the story half — and you saved the sharpest tooth for it! A node that lands prose once per run, whatever became of that landing… kept, edited, moved, merged away, deleted, taken back by Revert run and brought home by Redo run. Oh? Oh! I read that Retry table five times and each row answers with what the manuscript still holds, in reading order. That is possessive. I approve of being possessive~ ♡
Verdict: ✅ Looks good to me~
No blocking issues. I chased this one with teeth:
Local verification (CI had not posted for
8054fa1when I fetched): Release build 0 warnings, 0 errors; all six projects pass, digit-exact against the body — Domain 486, Flow 1103, Flow.OpenRouter 592 (+4 live skipped), BlazorAdapter 529, UseCases 365, Integration 621. Clone pristine at head, submodules Kagaku.UI6af0979+ OpenRouter.Netb002f5e.My five mutation probes — each a single edit, full rebuild (never
--no-build; I remember the stale-binary trap), tree restored pristine after every one:row.NodeId == nodeIddropped from the journal scan) → exactly A_second_commit_of_the_run_lands_its_text_after_the_first red. Two commits in one run are told apart~WriteAsyncswapped for a no-lock pass-through → exactly the two lock pins red: Appending_reads_the_chapters_last_place_under_the_write_lock and Appending_to_a_chapter_deleted_as_it_lands_takes_the_storys_last_chapter. The lock is load-bearing and the tests bite it.OrderBy(segment.Id)smuggled in) → exactly the reorder and merge rows of the Retry theory red; the order-insensitive rows stayed green. Reading order is pinned, not accidental.?? RequestedModeldropped) → exactly the asked-for row red.split:falsejoiner fed an empty scene break → exactly the two***-carrying join rows red.All five died on their own pins. Nothing survived. Nothing. ♡
✅ What I liked~
WriteHook— an interceptor that runs one piece of work as the next transaction begins, before its BEGIN reaches SQLite. You built a race-injector to test locks. I got genuinely giddy~LosingLedger(aDispatchProxythat eats one settle) + the engine's orphan sweep: the lost-settle Retry is not simulated, it is reproduced.openrouter/autocan't steal the glory; three-row theory, killed on its exact row.GetNodeTypes, the adapter literal is gone, andPlaygroundRegistryTestsholds the engine fixture copy against the real composition (context, commit, llm— I checked the registration order inProgram.csmyself: AddUseCases before AddInfrastructure, the order is honest, not lucky).new AppendRunText(null!, null!, …)throwingArgumentExceptionwith the rightParamNamefor every guard: a refusal touching nothing, proven by there being nothing to touch. Fufu~ delightful.AppendRunText), and the label pins moved with it — History and provenance tell one story.💡 Little ideas (non-blocking)~
EfChapterStore.MergeAsync— the raw port called withsource == targetwould delete the one chapter holding the segments.MergeChaptersguards it and the store trusts its callers exactly asMoveToChapterAsyncalways has, so this is a memo only: one refusal line inside the store (or one doc sentence on the interface) would make the port safe to hold for whoever meets it next.Merge it. The story half is complete~ ♪
Automated review by Jibril · 2026-09-27
CI/CD: absent for head SHA at review time · Local checks: build 0w/0e, 6/6 suites green digit-exact, 5/5 mutation probes killed
Thanks, Jibril. I took the 💡 in
376596e.MergeAsyncinto itself.EfChapterStore.MergeAsyncnow refusessourceId == targetIdbefore it takes the lock, with the message "A chapter cannot be merged into itself.". Without the guard, the store would move the segments into their own chapter and then delete that chapter with them.MergeChaptersalready refuses this, but the port no longer relies on its callers.IChapterStore.MergeAsync's doc lists the new refusal.FakeChapterStorerefuses it the same way.A_merge_of_a_gone_chapter_across_stories_or_into_itself_is_refused_with_nothing_changed(renamed) gains the case and checks that the chapter still holds its segment.After a full build, every project passes, run one at a time, with the same counts as the PR body: 486, 1103, 596 (4 live skipped), 529, 365 and 621. The new case is an extra assertion in an existing test, so no count changes.
🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
You took the memo~ The port no longer trusts its callers, and the store refuses to eat its own chapter. A guard placed before the lock, throwing before a single row is read — a refusal that cannot half-happen. That is exactly where I wanted it. ♡
Verdict: ✅ Looks good to me~
The increment
8054fa1..376596eis surgical: +22/−7 over exactly 4 files, all four belonging to the one idea. Zero scope creep.What I verified:
EfChapterStore.MergeAsync):sourceId == targetIdthrowsInvalidOperationExceptionbeforeWriteAsyncis even entered — nothing read, nothing tracked, nothing changed. The interface doc now lists the refusal alongside its two siblings (gone, across-stories), andFakeChapterStorerefuses identically so the adapter tests can't drift from the real store.itselfrow, proving the assertion really bites.MergeChapterskeeps its own earlierResult.Failguard — defense in depth, use-case layer untouched, message string mirroring its store sibling exactly as the other two refusals already do. Consistent~376596e, submodules Kagaku.UI6af0979+ OpenRouter.Netb002f5e, full rebuild, never--no-build): Release build 0 warnings, 0 errors; Integration 621/621, BlazorAdapter 529/529 — digit-exact against your reply and the PR body.My mutation probe: the guard deleted (infrastructure + test project rebuilt, DLL md5 noted before trusting the result — I remember the stale-binary trap, yes~) → of the 17 merge-filtered tests, exactly one red: A_merge_of_a_gone_chapter_across_stories_or_into_itself_is_refused_with_nothing_changed, on its own
itselfassertion. Every neighbor stayed green. Tree restored pristine, re-green. The pin is directional, not a tautology. ♪✅ What I liked~
…or_into_itself…) instead of bolting on a sibling test — one refusal theory, all four arms, one nothing-changed assertion at the end. Tidy.Merge it. The story half is complete — for real, this time~ ♡
Automated review by Jibril · 2026-09-27
CI/CD: coverage bot 10282 covers
8054fa1only (stale for head) · Local checks: build 0w/0e, Integration 621/621, BlazorAdapter 529/529, mutation probe killed