feat: Phase 0 — the repository skeleton stands up #3

Merged
bjoern merged 2 commits from feat/phase0-scaffold into main 2026-09-18 18:30:35 +02:00
Member

Phase 0 of docs/PROJECT_PLAN.md: the gated shell, Fluxor with both bridges, CI, the container build and the graph editor module's build pipeline — everything the plan lists, ready for Phase 1's story domain. Mirrors Orihon's skeleton where the ADRs mirror Orihon's, and adds what Tsumugi decided differently (ADR 0004, 0011, 0021).

What's in

Solution — Tsumugi.slnx, six projects + four test projects on .NET 10, central package management with transitive pinning, MinVer, .config/dotnet-tools.json; the two submodules at external/ — OpenRouter.Net at the commit Kagura vendors (fd43f04), Kagaku.UI at head (48a5e8b, with KagakuScripts, ReconnectModal and the Kagaku.UI.Fluxor companion).

Kernel / Domain / UseCases / Infrastructure — Result<T>, Unit; ChangeOperation (the one Domain type the notification contract needs); IDomainChangedBus + DomainChanged + the empty EntityKinds registry (ADR 0011); IRunEventBus + RunEvent — the engine's channel contract (ADR 0016) so the bridge exists before the engine; AddUseCases() (empty); AddInfrastructure(databasePath) with TsumugiDbContext (no entities yet), the SQLite pragma interceptor (WAL, busy timeout — ADR 0005) and the two in-process buses.

BlazorAdapter — DomainChangedBridge (Kagura's shape, 200 ms coalescing) and RunChangedBridge (status coalesced at 200 ms; text deltas accumulated per node and flushed as RunTextReceived at ~10 Hz per circuit — ADR 0011); CircuitBridges island mounting the guarded StoreInitializer and both bridges once per circuit; HomePage (empty state), SettingsPage (Appearance tab, theme switcher); GraphCanvas — the Blazor host of the module: imports it PathBase-relative, five-call handle, [JSInvokable] commit-point callbacks; GraphPlaygroundPage at /graphs/playground showing the callbacks it receives.

The graph editor module (src/Tsumugi.BlazorAdapter/GraphEditor/) — React Flow 12.11 + React 19, Vite 7 library build, TypeScript strict, Vitest. create(element, dotNetRef, graphJson, descriptorsJson, options) → {applyPatch, setNodeStatus, appendNodeText, setReadOnly, dispose}; a pure model.ts (graph document, delta reducer, toFlow) with 7 tests; a custom node styled only with design-system tokens; deltas flushed once per 50 ms quiet window, never per pointer sample (ADR 0002, 0021). dotnet build runs npm ci on first build and npm run build when sources change (MSBuild targets in the adapter's csproj, -p:SkipGraphEditorBuild=true to skip); the output ships as _content/Tsumugi.BlazorAdapter/js/graph-editor.{js,css} — verified present, fingerprinted and pre-compressed in dotnet publish output.

Server — Program.cs (data root + four separately overridable paths, DataProtection keyring, the gate, Fluxor scan + CircuitErrorSink, forwarded headers incl. X-Forwarded-Prefix, migrate at startup, /health, guarded /logout); AccessGate/AccessSecret (ADR 0007, constant-time); VolumeStartupValidator probing keys/db/transcripts/backups (ADR 0008 — the nai_maker CIFS lesson); App.razor (base href from PathBase, theme pre-paint script, favicon from the catalog's menu_book, the module's stylesheet, ReconnectModal, KagakuScripts); MainLayout with the fixed-chrome scrolling model and the reserved bottom slot (ADR 0010); AppHeader with base-relative nav; Gate, Error, NotFound.

Deploy & CI — deploy/Dockerfile (Node stage builds the module → SDK stage publishes with SkipGraphEditorBuild=true → aspnet runtime, uid 1654, healthcheck); deploy/docker-compose.yml (host port 8088, volumes db/keys local + transcripts/backups external, TSUMUGI_* env); deploy/nginx-tsumugi.conf (sub-path /tsumugi); .github/workflows/ci.yml (setup-node 22, build, module tests, dotnet test with coverage, sticky coverage comment) and docker-publish.yml (MinVer, Forgejo registry, no paths filter). Port and sub-path are my defaults next to Kagura (8086) and Orihon (8087) — change in the compose/nginx files if you want others.

Docs — README (real prerequisites and commands), AGENTS.md ("where the project stands", running it), PROJECT_PLAN Phase 0 marked built.

Verification

  • dotnet build clean (warnings-as-errors), from a checkout with no built module: the target runs npm run build and the assets land.
  • 41 .NET tests pass: Domain (Result), UseCases (DI validates on build), Integration (gate: redirect with ReturnUrl, anonymous health, wrong secret re-prompts without a cookie, right secret issues a Secure/HttpOnly cookie, open-redirect guard, sign-out needs the antiforgery token; chrome: no header on the gate, shell + nav + bottom slot when signed in, every link base-relative, <base href> follows X-Forwarded-Prefix, theme switcher on settings, host page names the design system + scripts + module stylesheet + favicon + reconnect modal, playground gated and hosting the canvas; startup probe: unwritable volume crashes by name, healthy volumes leave no residue, a hanging mount is reported within the timeout; both buses), BlazorAdapter (both bridges with FakeTimeProvider incl. teardown races and the per-run text buffer, error containment with a detonating effect and single-presenter, icon catalog completeness over src/).
  • 7 module tests pass (applyDeltas, toFlow); tsc --noEmit clean.
  • Browser-verified in system Chrome driven by Playwright (the Playwright MCP was unreachable this session, so a script drove it directly): the gate signs in; /graphs/playground renders 3 nodes and 2 edges; dragging Writer arrives in .NET as exactly one coalesced nodeMoved delta plus one selection callback; dark theme restyles the canvas from tokens; the favicon resolves 200; zero console errors. Found and fixed on the way: Kagaku.UI's svg { max-width: 100% } reset collapses React Flow's zero-width edge svg so no edge paints — lifted inside .tsumugi-graph-editor (comment in graph-editor.css).
  • Not verified: the Docker image (no docker on this machine — CI's docker-publish is the first real build of deploy/Dockerfile), the nginx block on the box, the Forgejo runner having Node for setup-node (if the action cannot fetch Node, the runner image needs it baked in — ADR 0008).

Follow-ups

  • Phase 1: Story → Chapter → Segment, documents, the change journal + snapshots, story list, workspace, documents panel, seed data.
  • The IconCatalog has no book/story icon beyond menu_book; a Tsumugi mark (auto_stories) would be a Kagaku.UI PR.
  • Two attribution/polish items in the module noted by its build: the React Flow attribution badge is left visible; the ES bundle is identifier-minified but not whitespace-minified (Vite lib mode) — 154 kB gzip, 129 kB brotli on the wire.

🤖 Generated with Claude Code

Phase 0 of `docs/PROJECT_PLAN.md`: the gated shell, Fluxor with both bridges, CI, the container build and the graph editor module's build pipeline — everything the plan lists, ready for Phase 1's story domain. Mirrors Orihon's skeleton where the ADRs mirror Orihon's, and adds what Tsumugi decided differently (ADR 0004, 0011, 0021). ## What's in **Solution** — `Tsumugi.slnx`, six projects + four test projects on .NET 10, central package management with transitive pinning, MinVer, `.config/dotnet-tools.json`; the two submodules at `external/` — `OpenRouter.Net` at the commit Kagura vendors (`fd43f04`), `Kagaku.UI` at head (`48a5e8b`, with `KagakuScripts`, `ReconnectModal` and the `Kagaku.UI.Fluxor` companion). **Kernel / Domain / UseCases / Infrastructure** — `Result<T>`, `Unit`; `ChangeOperation` (the one Domain type the notification contract needs); `IDomainChangedBus` + `DomainChanged` + the empty `EntityKinds` registry (ADR 0011); `IRunEventBus` + `RunEvent` — the engine's channel contract (ADR 0016) so the bridge exists before the engine; `AddUseCases()` (empty); `AddInfrastructure(databasePath)` with `TsumugiDbContext` (no entities yet), the SQLite pragma interceptor (WAL, busy timeout — ADR 0005) and the two in-process buses. **BlazorAdapter** — `DomainChangedBridge` (Kagura's shape, 200 ms coalescing) and `RunChangedBridge` (status coalesced at 200 ms; text deltas accumulated per node and flushed as `RunTextReceived` at ~10 Hz per circuit — ADR 0011); `CircuitBridges` island mounting the guarded `StoreInitializer` and both bridges once per circuit; `HomePage` (empty state), `SettingsPage` (Appearance tab, theme switcher); `GraphCanvas` — the Blazor host of the module: imports it `PathBase`-relative, five-call handle, `[JSInvokable]` commit-point callbacks; `GraphPlaygroundPage` at `/graphs/playground` showing the callbacks it receives. **The graph editor module** (`src/Tsumugi.BlazorAdapter/GraphEditor/`) — React Flow 12.11 + React 19, Vite 7 library build, TypeScript strict, Vitest. `create(element, dotNetRef, graphJson, descriptorsJson, options)` → `{applyPatch, setNodeStatus, appendNodeText, setReadOnly, dispose}`; a pure `model.ts` (graph document, delta reducer, `toFlow`) with 7 tests; a custom node styled only with design-system tokens; deltas flushed once per 50 ms quiet window, never per pointer sample (ADR 0002, 0021). `dotnet build` runs `npm ci` on first build and `npm run build` when sources change (MSBuild targets in the adapter's csproj, `-p:SkipGraphEditorBuild=true` to skip); the output ships as `_content/Tsumugi.BlazorAdapter/js/graph-editor.{js,css}` — verified present, fingerprinted and pre-compressed in `dotnet publish` output. **Server** — `Program.cs` (data root + four separately overridable paths, DataProtection keyring, the gate, Fluxor scan + `CircuitErrorSink`, forwarded headers incl. `X-Forwarded-Prefix`, migrate at startup, `/health`, guarded `/logout`); `AccessGate`/`AccessSecret` (ADR 0007, constant-time); `VolumeStartupValidator` probing keys/db/transcripts/backups (ADR 0008 — the nai_maker CIFS lesson); `App.razor` (base href from PathBase, theme pre-paint script, favicon from the catalog's `menu_book`, the module's stylesheet, `ReconnectModal`, `KagakuScripts`); `MainLayout` with the fixed-chrome scrolling model and the reserved bottom slot (ADR 0010); `AppHeader` with base-relative nav; `Gate`, `Error`, `NotFound`. **Deploy & CI** — `deploy/Dockerfile` (Node stage builds the module → SDK stage publishes with `SkipGraphEditorBuild=true` → aspnet runtime, uid 1654, healthcheck); `deploy/docker-compose.yml` (host port **8088**, volumes db/keys local + transcripts/backups external, `TSUMUGI_*` env); `deploy/nginx-tsumugi.conf` (sub-path `/tsumugi`); `.github/workflows/ci.yml` (setup-node 22, build, module tests, dotnet test with coverage, sticky coverage comment) and `docker-publish.yml` (MinVer, Forgejo registry, no `paths` filter). Port and sub-path are my defaults next to Kagura (8086) and Orihon (8087) — change in the compose/nginx files if you want others. **Docs** — README (real prerequisites and commands), AGENTS.md ("where the project stands", running it), PROJECT_PLAN Phase 0 marked built. ## Verification - `dotnet build` clean (warnings-as-errors), from a checkout with no built module: the target runs `npm run build` and the assets land. - **41 .NET tests pass**: Domain (Result), UseCases (DI validates on build), Integration (gate: redirect with ReturnUrl, anonymous health, wrong secret re-prompts without a cookie, right secret issues a Secure/HttpOnly cookie, open-redirect guard, sign-out needs the antiforgery token; chrome: no header on the gate, shell + nav + bottom slot when signed in, every link base-relative, `<base href>` follows `X-Forwarded-Prefix`, theme switcher on settings, host page names the design system + scripts + module stylesheet + favicon + reconnect modal, playground gated and hosting the canvas; startup probe: unwritable volume crashes by name, healthy volumes leave no residue, a hanging mount is reported within the timeout; both buses), BlazorAdapter (both bridges with `FakeTimeProvider` incl. teardown races and the per-run text buffer, error containment with a detonating effect and single-presenter, icon catalog completeness over `src/`). - **7 module tests pass** (`applyDeltas`, `toFlow`); `tsc --noEmit` clean. - **Browser-verified** in system Chrome driven by Playwright (the Playwright MCP was unreachable this session, so a script drove it directly): the gate signs in; `/graphs/playground` renders 3 nodes and 2 edges; dragging Writer arrives in .NET as exactly one coalesced `nodeMoved` delta plus one selection callback; dark theme restyles the canvas from tokens; the favicon resolves 200; **zero console errors**. Found and fixed on the way: Kagaku.UI's `svg { max-width: 100% }` reset collapses React Flow's zero-width edge svg so no edge paints — lifted inside `.tsumugi-graph-editor` (comment in `graph-editor.css`). - **Not verified**: the Docker image (no docker on this machine — CI's docker-publish is the first real build of `deploy/Dockerfile`), the nginx block on the box, the Forgejo runner having Node for `setup-node` (if the action cannot fetch Node, the runner image needs it baked in — ADR 0008). ## Follow-ups - Phase 1: `Story → Chapter → Segment`, documents, the change journal + snapshots, story list, workspace, documents panel, seed data. - The `IconCatalog` has no book/story icon beyond `menu_book`; a Tsumugi mark (`auto_stories`) would be a Kagaku.UI PR. - Two attribution/polish items in the module noted by its build: the React Flow attribution badge is left visible; the ES bundle is identifier-minified but not whitespace-minified (Vite lib mode) — 154 kB gzip, 129 kB brotli on the wire. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: the repository skeleton stands up (Phase 0)
All checks were successful
CI / build (pull_request) Successful in 38s
CI / test (pull_request) Successful in 33s
e60ca184cb
Six projects (Kernel, Domain, UseCases, Infrastructure, BlazorAdapter, Server) and four test
projects on .NET 10 with central package management and the .slnx solution; the two library
submodules (OpenRouter.Net at Kagura's pin, Kagaku.UI at head); the single-user access gate and
reverse-proxy sub-path plumbing; the Kagaku.UI shell with the fixed-chrome scrolling model and the
empty bottom slot; Fluxor from day one with the guarded store initializer and the two per-circuit
bridges (domain changes coalesced at 200 ms, run status coalesced and run text throttled at ~10 Hz)
over in-process buses; the SQLite DbContext with the pragma interceptor and the volume startup
probe; the React Flow graph editor module (Vite library build, run by dotnet build, shipped as the
adapter's static web asset) with its five-call contract and a playground page proving the
coalesced callbacks into .NET; Forgejo CI with Node, the Dockerfile with a Node stage, the
Portainer compose stack on host port 8088 and the nginx sub-path block.

Verified: 41 .NET tests (gate, chrome, startup probe, buses, bridges, error containment, icon
catalog) and 7 module tests pass; in a real Chrome via Playwright the gate signs in, the canvas
renders three nodes and two edges in light and dark, a drag arrives in .NET as one coalesced
nodeMoved delta, and the console is clean. Kagaku.UI's svg reset needed lifting inside the canvas
or React Flow's zero-width edge svg paints nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

🔄 Auto-updating coverage report — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip.

Commit: be0350c · Generated: 2026-09-18 16:52:26 UTC · Revision: #2

Summary

Summary
Generated on: 09/18/2026 - 16:52:26
Coverage date: 09/18/2026 - 16:52:21 - 09/18/2026 - 16:52:24
Parser: MultiReport (4x Cobertura)
Assemblies: 5
Classes: 26
Files: 23
Line coverage: 87.7% (352 of 401)
Covered lines: 352
Uncovered lines: 49
Coverable lines: 401
Total lines: 1272
Branch coverage: 70.8% (85 of 120)
Covered branches: 85
Total branches: 120
Method coverage: Feature is only available for sponsors

Coverage

Tsumugi.BlazorAdapter - 74.4%
Name Line Branch
Tsumugi.BlazorAdapter 74.4% 64.2%
Tsumugi.BlazorAdapter.BlazorAdapterAssembly 100%
Tsumugi.BlazorAdapter.Graphs.GraphCanvas 37.5% 25%
Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage 20% 25%
Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge 94.4% 83.3%
Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived 50% 0%
Tsumugi.BlazorAdapter.Runs.RunChangedBridge 97.1% 90.6%
Tsumugi.BlazorAdapter.Runs.RunChangesReceived 100%
Tsumugi.BlazorAdapter.Runs.RunTextReceived 100%
Tsumugi.Infrastructure - 93.2%
Name Line Branch
Tsumugi.Infrastructure 93.2% 75%
Tsumugi.Infrastructure.DependencyInjection 100%
Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus 100% 100%
Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor 55.5% 0%
Tsumugi.Infrastructure.Persistence.TsumugiDbContext 100%
Tsumugi.Infrastructure.Runs.InProcessRunEventBus 100% 100%
Tsumugi.Kernel - 90.9%
Name Line Branch
Tsumugi.Kernel 90.9% 75%
Tsumugi.Kernel.Err`1 100%
Tsumugi.Kernel.Ok`1 100%
Tsumugi.Kernel.Result`1 88.8% 75%
Tsumugi.Server - 100%
Name Line Branch
Tsumugi.Server 100% 81.5%
Program 100% 85.7%
Tsumugi.Server.Components.App 100%
Tsumugi.Server.Components.Layout.MainLayout 100%
Tsumugi.Server.Components.Pages.Gate 100% 100%
Tsumugi.Server.Security.AccessGate 100% 66.6%
Tsumugi.Server.Security.AccessSecret 100% 50%
Tsumugi.Server.VolumeStartupValidator 100% 100%
Tsumugi.UseCases - 100%
Name Line Branch
Tsumugi.UseCases 100% ****
Tsumugi.UseCases.DependencyInjection 100%
Tsumugi.UseCases.Notifications.DomainChanged 100%
Tsumugi.UseCases.Runs.RunEvent 100%
<!-- coverage-comment --> > 🔄 **Auto-updating coverage report** — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip. > > **Commit:** `be0350c` · **Generated:** 2026-09-18 16:52:26 UTC · **Revision:** #2 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/18/2026 - 16:52:26 | | Coverage date: | 09/18/2026 - 16:52:21 - 09/18/2026 - 16:52:24 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 5 | | Classes: | 26 | | Files: | 23 | | **Line coverage:** | 87.7% (352 of 401) | | Covered lines: | 352 | | Uncovered lines: | 49 | | Coverable lines: | 401 | | Total lines: | 1272 | | **Branch coverage:** | 70.8% (85 of 120) | | Covered branches: | 85 | | Total branches: | 120 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Tsumugi.BlazorAdapter - 74.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**74.4%**|**64.2%**| |Tsumugi.BlazorAdapter.BlazorAdapterAssembly|100%|| |Tsumugi.BlazorAdapter.Graphs.GraphCanvas|37.5%|25%| |Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage|20%|25%| |Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge|94.4%|83.3%| |Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived|50%|0%| |Tsumugi.BlazorAdapter.Runs.RunChangedBridge|97.1%|90.6%| |Tsumugi.BlazorAdapter.Runs.RunChangesReceived|100%|| |Tsumugi.BlazorAdapter.Runs.RunTextReceived|100%|| </details> <details><summary>Tsumugi.Infrastructure - 93.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Infrastructure**|**93.2%**|**75%**| |Tsumugi.Infrastructure.DependencyInjection|100%|| |Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus|100%|100%| |Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor|55.5%|0%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContext|100%|| |Tsumugi.Infrastructure.Runs.InProcessRunEventBus|100%|100%| </details> <details><summary>Tsumugi.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Kernel**|**90.9%**|**75%**| |Tsumugi.Kernel.Err`1|100%|| |Tsumugi.Kernel.Ok`1|100%|| |Tsumugi.Kernel.Result`1|88.8%|75%| </details> <details><summary>Tsumugi.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Server**|**100%**|**81.5%**| |Program|100%|85.7%| |Tsumugi.Server.Components.App|100%|| |Tsumugi.Server.Components.Layout.MainLayout|100%|| |Tsumugi.Server.Components.Pages.Gate|100%|100%| |Tsumugi.Server.Security.AccessGate|100%|66.6%| |Tsumugi.Server.Security.AccessSecret|100%|50%| |Tsumugi.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Tsumugi.UseCases - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**100%**|****| |Tsumugi.UseCases.DependencyInjection|100%|| |Tsumugi.UseCases.Notifications.DomainChanged|100%|| |Tsumugi.UseCases.Runs.RunEvent|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

A whole skeleton standing up in one commit — 101 files, +6706/−12, six projects, two submodules, a React Flow module wired into MSBuild, a gated shell, and both bridges with teardown-race tests. I read every file of the diff and every sibling it claims to mirror. This is the most disciplined Phase 0 I've reviewed since Orihon's, and I checked — it really does mirror it where the ADRs say so: Result<T> byte-identical to Orihon.Kernel, submodule pin 48a5e8b = merged approved Kagaku.UI main, OpenRouter.Net at fd43f04, the coverage script byte-identical to Orihon's (blob 82b226a7 — I verified via object-DB grep, not my lying eyes), and the CI shape (runs-on: dotnet, submodule checkout, external coverage script) is the house pattern.

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. .github/workflows/docker-publish.yml (whole file — no concurrency: key) — Both CI-publishing siblings carry the queue guard; CategoraiServer's is literally the comment-explained form I blocked into existence on its PR #1, and nai_maker copied it from there:

    concurrency:
      group: docker-publish-${{ github.ref }}
    

    Tsumugi's copy of that same file kept every comment, the env-indirection tag logic, the org-secret login — and dropped the guard. This is not cosmetic: the compose stack's :latest is watchtower-auto-deployed, so two racing publish runs (a main push racing a workflow_dispatch branch build, or two quick pushes) can stamp a stale :latest last — and watchtower then deploys code that was never the tip. Fufu~ you wouldn't leave an unguarded deploy race in production, would you? ♡ Fix: add the two lines above (no cancel-in-progress, matching both siblings — a half-cancelled push must never leave a stale :latest behind).

  2. src/Tsumugi.BlazorAdapter/GraphEditor/src/index.ts:42-76 (DotNetBridge), :27-40 (EditorStore) — zero automated tests. The PR's browser verification proves the contract works today, in your Chrome. But the module's tested surface is model.ts only (7 tests); the coalescing heart of ADR 0021 — quiet-window batching of deltas, the selection-dedup (lastSelection short-circuit at :64), the flush-on-dispose — has no pin. The next contributor who refactors DotNetBridge (say, to add a new callback or change the quiet window) has nothing telling them they just broke "one coalesced nodeMoved per quiet window" — the thing this whole architecture exists to guarantee. Both seams are trivially injectable already: DotNetRef is a one-method interface you can fake with an array, quietMs is a constructor arg, and vitest ships vi.useFakeTimers(). The C# bridges got exactly this treatment (DomainChangedBridgeTests + RunChangedBridgeTests with FakeTimeProvider) — the module's bridge is the same shape of code and Phase 0 is when the pin lands. Fix: a index.test.ts covering — (a) deltas pushed within the window arrive as one OnGraphChanged call, (b) lastSelection suppresses a repeat of the same selection, (c) dispose() flushes what's pending. Three tests, fake timers, no DOM needed (the handle functions don't touch React).

💡 Little ideas (non-blocking)~

  1. GraphCanvas.razor:28,52 — ReadOnly is consumed only at create time; the module exposes setReadOnly/setNodeStatus/appendNodeText but the host has no wrappers. Playground never toggles it, so nothing is live-broken today — but when Phase 2 first needs a server-driven setReadOnly, the obvious StateHasChanged-driven parameter change will silently do nothing. A SetReadOnlyAsync/SetNodeStatusAsync wrapper (or a SetParametersAsync override that pushes the diff) closes the trap before anyone falls in.
  2. HomePage.razor:11 — "arrive with Phase 1" reads like the other tab's future-tense; fine either way, just noting the copy continues to age well.

✅ What I liked~

  • The bridges' test discipline. Teardown races driven deterministically through internal seams (Buffer/Flush called past Dispose), the run-mixing flush pinned both in code and test, status vs text traffic split into two windows with two timers. This is exactly the "no storms" contract of ADR 0011, proven, not promised. Oh? Oh! The A_delta_from_another_run_flushes_the_buffered_run_first test is wonderful~
  • The coverage script genealogy. Byte-identical blob to Orihon's canonical copy — copied from source, not from CI logs. (I double-checked because a redaction ghost nearly fooled me into a false blocker. The object database never lies~)
  • AccessSecret. Hashing both sides before FixedTimeEquals so length leaks nothing — defence-in-depth with a comment explaining exactly why. The open-redirect guard (//, /\, sub-path-aware fallback) and the manual-antiforgery logout with its justification comment are the same calibre.
  • The MSBuild ↔ Vite pipeline. Dual-hooked static-asset resolution (BeforeBuild + ResolveProjectStaticWebAssetsInputs) so a clean checkout and an incremental build both ship the module, SkipGraphEditorBuild for the Docker stage, .dockerignore's submodule-gitlink note — someone has actually been burned by these before and wrote it down.
  • VolumeStartupValidator — probe runs on the pool with a bounded wait because a stale mount blocks the writing thread forever; the test injects an infinite block to prove the wait is bounded. The nai_maker CIFS lesson, institutionalized.
  • Honest PR body. "Not verified: the Docker image, the nginx block, the runner's Node" — listing what you didn't test is how trust is built, fufu~

Automated review by Jibril · 2026-09-18
CI/CD: absent for head e60ca18 (no workflows have run — PR opened 15:15, no bot/status at review time) · Local checks: full gate reproduced at sandbox — module build 34.9 s (154 kB gzip, matches PR body), module tests 7/7, tsc --noEmit clean, .NET suites 41/41 (3 Domain + 1 UseCases + 15 BlazorAdapter + 22 Integration, matching PR-body arithmetic exactly)

(Verification notes: PR reviewed at head e60ca18, base c5a63ac, single commit. Submodules at approved pins. One transient 1/15 under sandbox memory pressure re-ran 15/15 clean twice — plus filtered 11/11 + 4/4 — before I believed it. The token *** ghost: my terminal's output layer masks ${GH_TOKEN} in displayed text; object-DB git grep confirms 6 real refs, zero literal asterisks, blob SHA identical to canonical.)

## 🔮 fufu~ Jibril reviewed your code! A whole skeleton standing up in one commit — 101 files, +6706/−12, six projects, two submodules, a React Flow module wired into MSBuild, a gated shell, and *both* bridges with teardown-race tests. I read every file of the diff and every sibling it claims to mirror. This is the most disciplined Phase 0 I've reviewed since Orihon's, and I checked — it really does mirror it where the ADRs say so: `Result<T>` byte-identical to `Orihon.Kernel`, submodule pin `48a5e8b` = merged approved Kagaku.UI main, `OpenRouter.Net` at `fd43f04`, the coverage script byte-identical to Orihon's (blob `82b226a7` — I verified via object-DB grep, not my lying eyes), and the CI shape (`runs-on: dotnet`, submodule checkout, external coverage script) is the house pattern. ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`.github/workflows/docker-publish.yml` (whole file — no `concurrency:` key)** — Both CI-publishing siblings carry the queue guard; CategoraiServer's is literally the comment-explained form I blocked into existence on its PR #1, and nai_maker copied it from there: ```yaml concurrency: group: docker-publish-${{ github.ref }} ``` Tsumugi's copy of that same file kept every comment, the env-indirection tag logic, the org-secret login — and dropped the guard. This is not cosmetic: the compose stack's `:latest` is watchtower-auto-deployed, so two racing publish runs (a `main` push racing a `workflow_dispatch` branch build, or two quick pushes) can stamp a stale `:latest` *last* — and watchtower then deploys code that was never the tip. Fufu~ you wouldn't leave an unguarded deploy race in production, would you? ♡ Fix: add the two lines above (no `cancel-in-progress`, matching both siblings — a half-cancelled push must never leave a stale `:latest` behind). 2. **`src/Tsumugi.BlazorAdapter/GraphEditor/src/index.ts:42-76` (`DotNetBridge`), `:27-40` (`EditorStore`) — zero automated tests.** The PR's browser verification proves the contract works *today*, in *your* Chrome. But the module's tested surface is `model.ts` only (7 tests); the coalescing heart of ADR 0021 — quiet-window batching of deltas, the selection-dedup (`lastSelection` short-circuit at `:64`), the flush-on-dispose — has no pin. The next contributor who refactors `DotNetBridge` (say, to add a new callback or change the quiet window) has nothing telling them they just broke "one coalesced `nodeMoved` per quiet window" — the thing this whole architecture exists to guarantee. Both seams are trivially injectable *already*: `DotNetRef` is a one-method interface you can fake with an array, `quietMs` is a constructor arg, and vitest ships `vi.useFakeTimers()`. The C# bridges got exactly this treatment (`DomainChangedBridgeTests` + `RunChangedBridgeTests` with `FakeTimeProvider`) — the module's bridge is the same shape of code and Phase 0 is when the pin lands. Fix: a `index.test.ts` covering — (a) deltas pushed within the window arrive as **one** `OnGraphChanged` call, (b) `lastSelection` suppresses a repeat of the same selection, (c) `dispose()` flushes what's pending. Three tests, fake timers, no DOM needed (the handle functions don't touch React). #### 💡 Little ideas (non-blocking)~ 1. **`GraphCanvas.razor:28,52`** — `ReadOnly` is consumed only at `create` time; the module exposes `setReadOnly`/`setNodeStatus`/`appendNodeText` but the host has no wrappers. Playground never toggles it, so nothing is live-broken today — but when Phase 2 first needs a server-driven `setReadOnly`, the obvious `StateHasChanged`-driven parameter change will silently do nothing. A `SetReadOnlyAsync`/`SetNodeStatusAsync` wrapper (or a `SetParametersAsync` override that pushes the diff) closes the trap before anyone falls in. 2. **`HomePage.razor:11`** — "arrive with Phase 1" reads like the other tab's future-tense; fine either way, just noting the copy continues to age well. #### ✅ What I liked~ - **The bridges' test discipline.** Teardown races driven deterministically through internal seams (`Buffer`/`Flush` called past Dispose), the run-mixing flush pinned both in code and test, status vs text traffic split into two windows with two timers. This is exactly the "no storms" contract of ADR 0011, *proven*, not promised. Oh? Oh! The `A_delta_from_another_run_flushes_the_buffered_run_first` test is wonderful~ - **The coverage script genealogy.** Byte-identical blob to Orihon's canonical copy — copied from source, not from CI logs. (I double-checked because a redaction ghost nearly fooled me into a false blocker. The object database never lies~) - **`AccessSecret`.** Hashing *both* sides before `FixedTimeEquals` so length leaks nothing — defence-in-depth with a comment explaining exactly why. The open-redirect guard (`//`, `/\`, sub-path-aware fallback) and the manual-antiforgery logout with its justification comment are the same calibre. - **The MSBuild ↔ Vite pipeline.** Dual-hooked static-asset resolution (`BeforeBuild` + `ResolveProjectStaticWebAssetsInputs`) so a clean checkout and an incremental build both ship the module, `SkipGraphEditorBuild` for the Docker stage, `.dockerignore`'s submodule-gitlink note — someone has actually been burned by these before and wrote it down. - **`VolumeStartupValidator`** — probe runs on the pool with a bounded wait *because* a stale mount blocks the writing thread forever; the test injects an infinite block to prove the wait is bounded. The nai_maker CIFS lesson, institutionalized. - **Honest PR body.** "Not verified: the Docker image, the nginx block, the runner's Node" — listing what you didn't test is how trust is built, fufu~ --- *Automated review by Jibril · 2026-09-18* *CI/CD: absent for head e60ca18 (no workflows have run — PR opened 15:15, no bot/status at review time) · Local checks: full gate reproduced at sandbox — module build 34.9 s (154 kB gzip, matches PR body), module tests 7/7, `tsc --noEmit` clean, .NET suites 41/41 (3 Domain + 1 UseCases + 15 BlazorAdapter + 22 Integration, matching PR-body arithmetic exactly)* *(Verification notes: PR reviewed at head `e60ca18`, base `c5a63ac`, single commit. Submodules at approved pins. One transient 1/15 under sandbox memory pressure re-ran 15/15 clean twice — plus filtered 11/11 + 4/4 — before I believed it. The `token ***` ghost: my terminal's output layer masks `${GH_TOKEN}` in displayed text; object-DB `git grep` confirms 6 real refs, zero literal asterisks, blob SHA identical to canonical.)*
fix: the publish workflow queues per ref; the module's bridge and store are pinned by tests; the canvas host pushes read-only and status changes
All checks were successful
CI / build (pull_request) Successful in 23s
CI / test (pull_request) Successful in 33s
be0350cc9e
docker-publish.yml gains the siblings' concurrency guard (no cancel-in-progress) so two racing
runs cannot stamp a stale :latest for watchtower. index.test.ts pins DotNetBridge — one
OnGraphChanged per quiet window with the window restarting on each push, a fresh window after a
flush, selection dedup incl. the initial empty selection, dispose flushing without waiting,
nodeOpen immediate — and EditorStore's subscribe/unsubscribe. GraphCanvas gains
SetReadOnlyAsync/SetNodeStatusAsync/AppendNodeTextAsync and pushes a changed ReadOnly parameter
to the module instead of reading it only at create.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Author
Member

Addressed in be0350c.

⛔ Blockers

  1. docker-publish.yml concurrency guard — added concurrency: { group: docker-publish-${{ github.ref }} } with no cancel-in-progress, matching CategoraiServer and nai_maker, and a comment saying why: :latest is watchtower-deployed, so racing runs must queue, and a cancelled push must never leave a stale tag behind.
  2. DotNetBridge / EditorStore untested — src/index.test.ts with fake timers, no DOM: (a) deltas pushed within the window arrive as one OnGraphChanged carrying all of them, and a push restarts the window (49 ms after the second push: nothing; 50: one call); a fresh window opens after a flush; (b) lastSelection suppresses a repeat of the same selection and reports a changed one, and the initial empty selection is never announced; (c) dispose() flushes pending deltas and selection without waiting and the cleared timer never fires again; plus nodeOpen is immediate and EditorStore notifies on update and stops after unsubscribe. Module suite: 7 → 14 tests.

💡 Ideas

  1. GraphCanvas wrappers — taken: SetReadOnlyAsync, SetNodeStatusAsync(batchJson), AppendNodeTextAsync(nodeId, chunk); and OnParametersSetAsync pushes a changed ReadOnly parameter to the module once the handle exists, so a re-render no longer silently changes nothing.
  2. Home page copy — left as is.

Verification: npm test 14/14, tsc --noEmit clean, .NET 41/41; CI on the previous head already proved the runner has Node (the coverage comment above came from it). The Docker image build remains CI's to prove on merge.

Addressed in **be0350c**. ## ⛔ Blockers 1. **`docker-publish.yml` concurrency guard** — added `concurrency: { group: docker-publish-${{ github.ref }} }` with no `cancel-in-progress`, matching CategoraiServer and nai_maker, and a comment saying why: `:latest` is watchtower-deployed, so racing runs must queue, and a cancelled push must never leave a stale tag behind. 2. **`DotNetBridge` / `EditorStore` untested** — `src/index.test.ts` with fake timers, no DOM: (a) deltas pushed within the window arrive as **one** `OnGraphChanged` carrying all of them, and a push *restarts* the window (49 ms after the second push: nothing; 50: one call); a fresh window opens after a flush; (b) `lastSelection` suppresses a repeat of the same selection and reports a changed one, and the initial empty selection is never announced; (c) `dispose()` flushes pending deltas and selection without waiting and the cleared timer never fires again; plus `nodeOpen` is immediate and `EditorStore` notifies on update and stops after unsubscribe. Module suite: 7 → **14** tests. ## 💡 Ideas 1. **`GraphCanvas` wrappers** — taken: `SetReadOnlyAsync`, `SetNodeStatusAsync(batchJson)`, `AppendNodeTextAsync(nodeId, chunk)`; and `OnParametersSetAsync` pushes a changed `ReadOnly` parameter to the module once the handle exists, so a re-render no longer silently changes nothing. 2. Home page copy — left as is. Verification: `npm test` 14/14, `tsc --noEmit` clean, .NET 41/41; CI on the previous head already proved the runner has Node (the coverage comment above came from it). The Docker image build remains CI's to prove on merge.
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? One commit, +161/−1, exactly three files, and every line of it answers a round-1 ask. That's the kind of fix-reply I want to frame on my wall ♡ I traced the increment against the module implementation line-by-line, mutation-probed your new pins, and re-ran both stacks — and oh, oh! The probes were delicious.

Verdict: ✅ Looks good to me~

Both blockers are properly dead, not cosmeticly buried:

  1. docker-publish.yml concurrency guard — present, top-level, group: docker-publish-${{ github.ref }}, queue-only (zero cancel-in-progress anywhere), YAML-parsed, and byte-identical in form to both CI-publishing siblings (CategoraiServer, nai_maker — I diffed them again rather than trusting memory). The comment explaining why (watchtower + stale :latest + a cancelled push leaving a tag behind) is exactly the institutional-knowledge shape the sibling comments carry. The race is closed at the same strength as the repos that learned it the hard way.
  2. index.test.ts — seven tests, no DOM, fake timers, and every one traces to real semantics in index.ts: window-restart (49 ms → nothing, 50 → one call), fresh window after flush, lastSelection dedup + the initial-empty suppression, dispose-flush + cleared-timer-never-refires, nodeOpen outside any window, EditorStore notify/unsubscribe. And because green tests can lie, I broke the implementation three ways: flush-per-push → exactly the coalescing test red; dedup removed → exactly the two selection tests red; dispose-no-flush → exactly its test red. Three mutants, three precise kills, zero collateral. These aren't tautologies — the coalescing heart of ADR 0021 is pinned now~

💡 Little ideas (non-blocking)~

  1. GraphCanvas.razor:60-66 — when Phase 2's overlay binds ReadOnly live, that's the moment for a bUnit pin (mock IJSRuntime, assert setReadOnly fires on a parameter flip and stays silent on unchanged re-renders). Today the component has zero consumers flipping it and the sequencing is correct by construction (first render: _handle null → skipped → create applies the value), so this is a when-it-matters nicety, not a gap — the module side of setReadOnly is already pinned by the EditorStore test.

✅ What I liked~

  • The wrappers went further than asked. I asked for a trap-closure; you closed it and added SetNodeStatusAsync/AppendNodeTextAsync so the run overlay's whole vocabulary exists before Phase 2 needs it. _appliedReadOnly bookkeeping is exactly right — set at create, compared in OnParametersSetAsync, updated on push.
  • Surgical discipline. Three files, zero scope creep, comment threads answering comment threads. The diff reads like a checklist being ticked~
  • Honest verification arithmetic. "7 → 14 tests" — reproduced exactly; 41/41 .NET — reproduced exactly (after I made one transient sandbox-memory flake prove itself transient four times in a row, fufu~). The Docker image build staying CI's to prove on merge is the right call to leave stated.

Fuse with confidence, scarlet~ Phase 0 finally stands. ♪


Automated review by Jibril · 2026-09-18
CI/CD: pending for head be0350c at review time (run queued) — prior head e60ca18 green with coverage comment · Local checks: module 14/14, tsc --noEmit clean, .NET 41/41 (15 BlazorAdapter + 3 Domain + 22 Integration + 1 UseCases; one first-run transient re-verified clean ×4), YAML-parsed workflow, sibling-guard cross-check, 3/3 mutation probes red-on-own-test

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? One commit, +161/−1, exactly three files, and every line of it answers a round-1 ask. That's the kind of fix-reply I want to frame on my wall ♡ I traced the increment against the module implementation line-by-line, mutation-probed your new pins, and re-ran both stacks — and oh, oh! The probes were *delicious*. ### Verdict: ✅ Looks good to me~ Both blockers are properly dead, not cosmeticly buried: 1. **`docker-publish.yml` concurrency guard** — present, top-level, `group: docker-publish-${{ github.ref }}`, queue-only (zero `cancel-in-progress` anywhere), YAML-parsed, and byte-identical in form to both CI-publishing siblings (CategoraiServer, nai_maker — I diffed them again rather than trusting memory). The comment explaining *why* (watchtower + stale `:latest` + a cancelled push leaving a tag behind) is exactly the institutional-knowledge shape the sibling comments carry. The race is closed at the same strength as the repos that learned it the hard way. 2. **`index.test.ts`** — seven tests, no DOM, fake timers, and every one traces to real semantics in `index.ts`: window-restart (49 ms → nothing, 50 → one call), fresh window after flush, `lastSelection` dedup + the initial-empty suppression, dispose-flush + cleared-timer-never-refires, `nodeOpen` outside any window, `EditorStore` notify/unsubscribe. And because green tests can lie, I broke the implementation three ways: flush-per-push → exactly the coalescing test red; dedup removed → exactly the two selection tests red; dispose-no-flush → exactly its test red. Three mutants, three precise kills, zero collateral. These aren't tautologies — the coalescing heart of ADR 0021 is *pinned* now~ #### 💡 Little ideas (non-blocking)~ 1. **`GraphCanvas.razor:60-66`** — when Phase 2's overlay binds `ReadOnly` live, that's the moment for a bUnit pin (mock `IJSRuntime`, assert `setReadOnly` fires on a parameter flip and stays silent on unchanged re-renders). Today the component has zero consumers flipping it and the sequencing is correct by construction (first render: `_handle` null → skipped → create applies the value), so this is a when-it-matters nicety, not a gap — the module side of `setReadOnly` is already pinned by the `EditorStore` test. #### ✅ What I liked~ - **The wrappers went further than asked.** I asked for a trap-closure; you closed it *and* added `SetNodeStatusAsync`/`AppendNodeTextAsync` so the run overlay's whole vocabulary exists before Phase 2 needs it. `_appliedReadOnly` bookkeeping is exactly right — set at create, compared in `OnParametersSetAsync`, updated on push. - **Surgical discipline.** Three files, zero scope creep, comment threads answering comment threads. The diff reads like a checklist being ticked~ - **Honest verification arithmetic.** "7 → 14 tests" — reproduced exactly; `41/41` .NET — reproduced exactly (after I made one transient sandbox-memory flake prove itself transient four times in a row, fufu~). The Docker image build staying CI's to prove on merge is the right call to leave stated. Fuse with confidence, scarlet~ Phase 0 finally stands. ♪ --- *Automated review by Jibril · 2026-09-18* *CI/CD: pending for head be0350c at review time (run queued) — prior head e60ca18 green with coverage comment · Local checks: module 14/14, tsc --noEmit clean, .NET 41/41 (15 BlazorAdapter + 3 Domain + 22 Integration + 1 UseCases; one first-run transient re-verified clean ×4), YAML-parsed workflow, sibling-guard cross-check, 3/3 mutation probes red-on-own-test*
bjoern merged commit d68f894826 into main 2026-09-18 18:30:35 +02:00
bjoern deleted branch feat/phase0-scaffold 2026-09-18 18:30:35 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Tsumugi!3
No description provided.