No description
  • C# 86.6%
  • TypeScript 6.7%
  • HTML 5.4%
  • CSS 0.8%
  • JavaScript 0.3%
  • Other 0.1%
Find a file
scarlet 57304d6e08
Some checks failed
CI / build (push) Successful in 55s
CI / editor-browsers (push) Successful in 2m38s
Build and Push Docker Image / build-and-push (push) Successful in 1m11s
CI / test (push) Failing after 4m58s
feat(runs): parallel streams on the run page — text per row, iterations grouped and named in words, the preview for append-bound streams only (Phase 3 slice 16) (#120)
Phase 3's slice 16, parallel streams on the run page (ADR 0024, 0027, Phase 3 decisions 18 and 19). Several writers can now stream in one run, and the run's page keeps them apart:
- every row of a run streams its own text;
- a container's iterations are grouped under it and named in plain words, *Writers · 2 of 3*;
- the chapter's preview shows only a stream bound for an append into that chapter.

The total in "2 of 3" comes from the container type when it starts its iterations (a map's items). Björn's choice. The ledger keeps it with the expansion, so no migration is needed. No container type ships yet (`map` is slice 17), so the tests and the browser pass build container rows by hand, the way the ledger's Expand leaves them.

## What changes

**Engine (Kagaku.Flow): iteration totals.**
- `ContainerExpansion` keeps a `Total` beside `Started`, never below it. `ContainerPlan.Start(iterations, total)` says it.
- The step writes the total with the expansion:
  - a later start's total replaces it;
  - a start that gives none keeps it;
  - the total of a start of no iteration is not kept: the engine reads such a start as a wait, which writes nothing.
- A plan that starts more iterations than its total fails the container ("The container's plan starts *n* iterations, more than its total of *t*.").
- The EF ledger stores the total inside the expansion's JSON. RunLedgerContract's sample carries one.
- The expansion log now reads "*m* so far". "In all" now means the total.

**Live text per row.**
- `RunPreviewState` keeps every row's text (node and iteration path). Each row carries the run's start number, and a row that starts afresh takes the next one.
- The monitor reads each row's own text, so parallel writers stream side by side.

**The chapter's preview.**
- `PreviewedStreams` works out from the run's snapshot which top-level `llm`'s text reaches an append into the target chapter. It follows the text straight, through a branch or a transform, or through the auto-commit. `GetStoryRunState` hands the result to the page.
- The preview shows the latest of those streams to start. It never shows an iteration's stream, or one bound for a new chapter, a replace or an insert.
- A selection run previews nothing, an append's included, and shows its working state alone (decision 18, ADR 0030).

**Iterations in words.**
- `IterationNames` turns a path into *Writers · 2 of 3*:
  - the container's title comes from the run's graph, its id when the graph gives none;
  - the total comes from the container row's expansion; without one the name reads *Writers · 2*;
  - nested iterations join outermost first;
  - a path the engine did not write reads as itself.
- `RunSteps` sets the order:
  - steps are listed by when each first started; a container makes no attempt of its own, so it counts from its expansion;
  - a container's iterations follow right after its row, the first first, one level further in;
  - a row whose container the run does not hold stays at the top level.
- Where the words show:
  - **monitor:** the iteration's words in their own column, each level indented;
  - **inspector:** the words, with the raw path behind "Its iteration path";
  - **transcript:** the words beside the path;
  - **inspector and transcript, on a container:** that its iterations make the attempts (`StepText.NoAttempt`);
  - **strip:** *Writer in Writers · 2 of 3*. A failed container gives way to the step that failed in it, and a running one to the steps in it that run or wait.
- A commit names the iteration as the run names it when the text lands, from the commit node and from Commit this text. It goes into History's label (*Land in Writers · 2 of 3 appended a paragraph*) and into the paragraph's credit:
  - `Provenance` keeps the words under a new key, so the gutter's title shows them without reading the run (*Run · writer in Writers · 2 of 3 · model*) and they outlive a cleared run history;
  - a merge compares who wrote two paragraphs, not these words.

**Retry on the run's page.**
- Every failed step's row of a run that was not cancelled offers Retry, a failed iteration's among them.
- A "Steps" heading above the list explains Retry behind its tip, which stands outside the list's scrolling. The strip's expanded list offers no Retry.
- While a request is out, every Retry and the page's own action wait, and only the pressed one shows busy.
- Once the answer is in, `focus-keeper.js`'s new `refocusWithin` gives the focus back to the button it was on, or to the step's name once the button is gone. It does nothing if the writer has moved the focus meanwhile. After a refusal it waits until a read since the press has landed.
- In the tests, `ClickPastDisabledAsync` moves into `StoryPageTestContext`. Two inspector tests clicked a disabled button, which bUnit silently drops; they now click past it and exercise their guards.

**Docs.**
- ADR 0016 (iteration totals), 0012 (iterations in words, with the provenance shape) and 0024 (as built) amended.
- Engine README D5, L1 and P17.
- `story-workspace.md` and `run-monitor-and-inspector.md` ticked.
- The plan ticks slice 16 and hands slice 17 the strip's sentence for a map with no items.

## Not in this slice

- **The `map` and `loop` types** (17, 18). Until then, a run can't make a container row.
- **The strip saying a run whose map had no items wrote nothing** (ADR 0024's Phase 3 decisions) needs the map type, so it moves to slice 17's line.
- **The run's graph view** with the execution overlay (slice 29).

## Verification

- **.NET tests.** All six projects pass under TZ=UTC and in the local zone:

  | Project | Passed |
  |---|---|
  | Domain | 573 |
  | Kagaku.Flow | 1758 |
  | Kagaku.Flow.OpenRouter | 608, plus 5 skipped live checks |
  | UseCases | 941 |
  | BlazorAdapter | 1216 |
  | Integration | 1240 |

  The manuscript module's vitest suite passes too (344), with the focus keeper's new cases.
- **What the tests pin:**
  - **Totals:** kept until a later start gives another, and a plan beyond its total failing the container, pending and running.
  - **The preview rule:** straight, through a branch or a transform, the auto-commit, and nested, json, tool, other-sink, replace, insert and new-chapter cases. A selection run previews nothing for any mode.
  - **The preview's choice:** the page shows the latest stream to start, never an iteration's.
  - **Names and order:** malformed paths, overflow, totals looked up by node and scope with prefix-sharing ids, and orphans at depth 0.
  - **Real-shaped containers:** container rows as Expand leaves them, with no attempt. A step that started after a container stays below it.
  - **Strip, labels, credits and gutter:** the give-way cases, including the transient state between failing a step and failing its container; History's label and the credit with and without a total, from both commit paths; the gutter's title; and the merge that ignores the words.
  - **Retry:** busy per button, double-press guards, a refusal that keeps the Retry, a refusal whose read takes it away (focus held until that read lands), and no Retry in a cancelled run.
- **Coverage of the changed .NET lines:** 291/291 lines, 240/240 branches.
- **Browser pass**, in Chrome against the seeded world and the OpenRouter stub (notes in the slice's browser-pass record):
  - **An iterated run written into the ledger by hand.** The page groups and indents the three writers, reads *Writers · 2 of 3*, and offers Retry on both failed rows. The strip says "failed at Writer in Writers · 2 of 3". The inspector shows the words, the path behind its disclosure, and the container's no-attempt sentence. The tip opens unclipped above the list.
  - **A row's Retry.** It retried the run. With no `map` type yet, the engine then failed the container through the failed view, as slice 15c rules. The focus went to the step's name.
  - **Continue's preview.** It streamed at the chapter's end and went away when the run settled.
  - **A Rewrite selection run.** It showed its working state and no preview.
- **Mutation probes:** 86 of 86 killed, re-run over the final tests:
  - 58 on the slice;
  - 18 on the round-0 fixes;
  - 10 on the pass-2 fixes.

  Two mutants survived along the way, and each got a test that kills it: Commit this text's read of the run's rows (a case with a container total), and a running container beside only a failed step (the strip's state between the two passes). For the final run, six slice mutants were re-aimed at their lines as the fixes left them, and three were dropped as duplicates of fix probes (60 → 58, 19 → 18). Two were rewritten so they compile under nullable flow analysis.
- **Reviews.** The counts come from the saved review JSON.
  - **Round 0:** 27 findings (22 confirmed, 5 partly; 3 blockers, 8 suggestions, 16 nits), 23 distinct issues. 25 were taken and 2 taken in part. The blockers:
    - real container rows make no attempt, so they sorted as never started;
    - the double-press test clicked a disabled button, which bUnit drops;
    - an append-mode selection run was previewed, against decision 18.

    The two partial ones:
    - naming an orphan's iteration by its container's id stays, as the amendment says;
    - the inspector header's list follows its sections, not the render order.
  - **Second pass:** all 27 round-0 points checked: 23 resolved, 4 partly. Two of the four are declined parts the checker judged sound; the other two gaps were closed in this pass. It found 32 new points (29 confirmed, 2 partly, 1 refuted; 8 suggestions, 24 nits), 18 distinct issues. 28 were taken and 2 taken in part:
    - the inspector's container wording;
    - the refocus that could land on a button the read then removed;
    - the per-row tips the list's scrolling clipped, now one tip beside the heading;
    - a retried container's time, now documented;
    - several tests;
    - wording and widths.

    Declined, with reasons:
    - one focus case that needs two moves within the request's short window; `regain` has the same limit for one click on blank page;
    - building the negative-total message with `Environment.NewLine`: copied from a run on the Linux CI platform, and no test in the repo builds one;
    - a shared constant for the two Retry tips, which word it differently.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: schattenan <schattenan@kagaku.eu>
Reviewed-on: #120
2026-10-18 13:32:54 +02:00
.claude/skills/pr-review-loop docs: the PR review loop with Jibril as a repo skill (#114) 2026-10-17 17:44:06 +02:00
.config feat: the repository skeleton stands up (Phase 0) 2026-09-18 15:14:39 +02:00
.github feat(runs): the status strip — the run that needs watching in the bottom slot, Cancel behind its question, Dismiss into the ledger (slice 13a) (#88) 2026-10-05 23:40:21 +02:00
deploy fix(deploy): the image can never ship without the Blazor script (#119) 2026-10-18 12:27:06 +02:00
docs feat(runs): parallel streams on the run page — text per row, iterations grouped and named in words, the preview for append-bound streams only (Phase 3 slice 16) (#120) 2026-10-18 13:32:54 +02:00
external fix(ci): a dismissed error stays dismissed — Kagaku.UI's error sink, bumped, ends the store-failure page tests' timeouts (#93) 2026-10-07 15:46:35 +02:00
src feat(runs): parallel streams on the run page — text per row, iterations grouped and named in words, the preview for append-bound streams only (Phase 3 slice 16) (#120) 2026-10-18 13:32:54 +02:00
tests feat(runs): parallel streams on the run page — text per row, iterations grouped and named in words, the preview for append-bound streams only (Phase 3 slice 16) (#120) 2026-10-18 13:32:54 +02:00
.dockerignore feat(editor): the manuscript surface module on its harness page (editor track M2a) 2026-09-26 02:44:12 +02:00
.gitattributes feat: the repository skeleton stands up (Phase 0) 2026-09-18 15:14:39 +02:00
.gitignore feat(runs): a cut-off attempt's cost — looked up after the fact and counted, "Looking up…" until it lands (slice 14e) (#96) 2026-10-09 21:23:53 +02:00
.gitmodules feat: the repository skeleton stands up (Phase 0) 2026-09-18 15:14:39 +02:00
AGENTS.md feat(engine): the container scheduler — a container type plans its body, the pass expands, extends, collects and fails it (Phase 3 slice 15b) (#117) 2026-10-18 08:59:58 +02:00
CLAUDE.md chore: the repository opens docs-first 2026-09-18 13:17:24 +02:00
Directory.Build.props feat: the repository skeleton stands up (Phase 0) 2026-09-18 15:14:39 +02:00
Directory.Packages.props feat(flow): the llm node's config and request — its sockets and templates, every bad setting refused by name, strict schemas, the budget answer, and routing that inherits the install-wide defaults 2026-09-26 03:16:36 +02:00
README.md feat(graphs): the selection templates — Expand, Rewrite and Continue from here (Phase 3 slice 9) (#109) 2026-10-17 02:31:16 +02:00
Tsumugi.slnx feat(flow): the run ledger — records, pure transitions, the port, the trigger envelope and run events; Kagaku.Flow.Testing's in-memory ledger and contract 2026-09-25 15:49:57 +02:00

Tsumugi

A single-user, self-hosted agentic story-writing app — a Blazor Server app that grows a story through a user-editable generation graph: from a one-node "continue the story" up to planner → your choice → detail planner → writer → refiner → commit, with loops, tool calls, and pauses for your input. Every LLM call goes through OpenRouter; every run is inspectable and resumable.

Status: Phase 2 complete. The architecture (each ADR's status is in docs/adr/README.md), requirements, plan and user stories are settled; Phase 0 (the gated shell, the Fluxor bridges, CI, the container build, the graph editor module's build pipeline), Phase 1 — a manual manuscript tool with categories, documents, history, undo and nightly backups, as story pages (general, documents, one page per chapter, history) — and Phase 2 — the engine running the Continue graph from the Run button, streaming into the chapter, with the story's Runs section (monitor, inspector, costs, transcripts) and Revert run in History — are built, beside the manuscript editor track — its selection menu running selection graphs, the Expand, Rewrite and Continue from here templates among them, built in Phase 3's slices 5 to 9 — and the editor appearance, writing aids and inline suggestions tracks. Phase 3, the graph editor and the full node vocabulary, is under way.

Tsumugi (紡ぎ, "weaving" — monogatari o tsumugu, to weave a story) is a story-writing app whose generation pipeline is a graph the writer draws.

Prerequisites

  • .NET SDK 10.0+ and Node 22+ (the graph editor is a built frontend module — ADR 0004, 0021)
  • Access to the vendored submodules on git.kagaku.eu (TeamAI org)

Getting started

# Clone WITH submodules (OpenRouter.Net, Kagaku.UI live under external/)
git clone --recurse-submodules <tsumugi-url>
# Already cloned without them?
git submodule update --init --recursive

dotnet build   Tsumugi.slnx --configuration Release   # also builds both editor modules (npm ci + vite)
dotnet test    Tsumugi.slnx --configuration Release
dotnet run   --project src/Tsumugi.Server --launch-profile http   # http://localhost:5310, access secret "dev"

The graph editor module lives in src/Tsumugi.BlazorAdapter/GraphEditor/ (React Flow + Vite), the manuscript editor module in src/Tsumugi.BlazorAdapter/ManuscriptEditor/ (ProseMirror + Vite); each one's own checks are npm test and npm run typecheck there. dotnet build runs npm ci on first build and npm run build whenever a module's sources change; pass -p:SkipModuleBuild=true to skip both when the output is already in place (the Dockerfile does).

Layout

Project Responsibility
src/Tsumugi.Kernel Technical primitives (Result<T>). References nothing.
src/Tsumugi.Domain Entities and value objects. Pure — no I/O, no framework attributes.
src/Kagaku.Flow* The generic graph engine (core, OpenRouter nodes, later the editor) — no reference to Tsumugi (ADR 0025).
src/Tsumugi.UseCases Application layer: use cases, ports, and the story half of the engine.
src/Tsumugi.Infrastructure Driven adapters. The only project that sees the vendored clients.
src/Tsumugi.BlazorAdapter Primary UI adapter (Razor Class Library), incl. the graph editor's JS module.
src/Tsumugi.Server ASP.NET host + composition root.
tests/* Unit, integration and component test projects.
external/* Vendored libraries as git submodules (OpenRouter.Net, Kagaku.UI).
docs/ Architecture overview, project plan, requirements, ADRs, user stories.

Dependency direction: Kernel → Domain → UseCases → {Infrastructure, BlazorAdapter} → Server, with Kagaku.Flow referenced by UseCases, Kagaku.Flow.OpenRouter by Infrastructure, and neither referencing anything of Tsumugi's — enforced by the project-reference graph and by the architecture tests every Kagaku.Flow* test project runs over the engine assemblies in its output, its own included (Kagaku_Flow_references_no_Tsumugi_assembly, ADR 0025).

Conventions

  • .slnx solution format; Directory.Build.props + Directory.Packages.props (central package management).
  • net10.0, nullable + implicit usings on, warnings-as-errors, deterministic, invariant globalization.
  • Versioning via MinVer (v-prefixed tags).
  • CI on Forgejo Actions (.github/workflows/); container image published to git.kagaku.eu/teamai/tsumugi.
  • Docs: docs/adr/ (Nygard ADRs), docs/REQUIREMENTS.md (the tracker), docs/stories/ (user stories).