feat: Phase 0 — the repository skeleton stands up #3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/phase0-scaffold"
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?
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 atexternal/—OpenRouter.Netat the commit Kagura vendors (fd43f04),Kagaku.UIat head (48a5e8b, withKagakuScripts,ReconnectModaland theKagaku.UI.Fluxorcompanion).Kernel / Domain / UseCases / Infrastructure —
Result<T>,Unit;ChangeOperation(the one Domain type the notification contract needs);IDomainChangedBus+DomainChanged+ the emptyEntityKindsregistry (ADR 0011);IRunEventBus+RunEvent— the engine's channel contract (ADR 0016) so the bridge exists before the engine;AddUseCases()(empty);AddInfrastructure(databasePath)withTsumugiDbContext(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) andRunChangedBridge(status coalesced at 200 ms; text deltas accumulated per node and flushed asRunTextReceivedat ~10 Hz per circuit — ADR 0011);CircuitBridgesisland mounting the guardedStoreInitializerand both bridges once per circuit;HomePage(empty state),SettingsPage(Appearance tab, theme switcher);GraphCanvas— the Blazor host of the module: imports itPathBase-relative, five-call handle,[JSInvokable]commit-point callbacks;GraphPlaygroundPageat/graphs/playgroundshowing 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 puremodel.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 buildrunsnpm cion first build andnpm run buildwhen sources change (MSBuild targets in the adapter's csproj,-p:SkipGraphEditorBuild=trueto skip); the output ships as_content/Tsumugi.BlazorAdapter/js/graph-editor.{js,css}— verified present, fingerprinted and pre-compressed indotnet publishoutput.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);VolumeStartupValidatorprobing 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'smenu_book, the module's stylesheet,ReconnectModal,KagakuScripts);MainLayoutwith the fixed-chrome scrolling model and the reserved bottom slot (ADR 0010);AppHeaderwith base-relative nav;Gate,Error,NotFound.Deploy & CI —
deploy/Dockerfile(Node stage builds the module → SDK stage publishes withSkipGraphEditorBuild=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) anddocker-publish.yml(MinVer, Forgejo registry, nopathsfilter). 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 buildclean (warnings-as-errors), from a checkout with no built module: the target runsnpm run buildand the assets land.<base href>followsX-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 withFakeTimeProviderincl. teardown races and the per-run text buffer, error containment with a detonating effect and single-presenter, icon catalog completeness oversrc/).applyDeltas,toFlow);tsc --noEmitclean./graphs/playgroundrenders 3 nodes and 2 edges; dragging Writer arrives in .NET as exactly one coalescednodeMoveddelta 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'ssvg { max-width: 100% }reset collapses React Flow's zero-width edge svg so no edge paints — lifted inside.tsumugi-graph-editor(comment ingraph-editor.css).deploy/Dockerfile), the nginx block on the box, the Forgejo runner having Node forsetup-node(if the action cannot fetch Node, the runner image needs it baked in — ADR 0008).Follow-ups
Story → Chapter → Segment, documents, the change journal + snapshots, story list, workspace, documents panel, seed data.IconCataloghas no book/story icon beyondmenu_book; a Tsumugi mark (auto_stories) would be a Kagaku.UI PR.🤖 Generated with Claude Code
Summary
Summary
Coverage
Tsumugi.BlazorAdapter - 74.4%
Tsumugi.Infrastructure - 93.2%
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 100%
🔮 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 toOrihon.Kernel, submodule pin48a5e8b= merged approved Kagaku.UI main,OpenRouter.Netatfd43f04, the coverage script byte-identical to Orihon's (blob82b226a7— 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~
.github/workflows/docker-publish.yml(whole file — noconcurrency: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: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
:latestis watchtower-auto-deployed, so two racing publish runs (amainpush racing aworkflow_dispatchbranch build, or two quick pushes) can stamp a stale:latestlast — 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 (nocancel-in-progress, matching both siblings — a half-cancelled push must never leave a stale:latestbehind).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 ismodel.tsonly (7 tests); the coalescing heart of ADR 0021 — quiet-window batching of deltas, the selection-dedup (lastSelectionshort-circuit at:64), the flush-on-dispose — has no pin. The next contributor who refactorsDotNetBridge(say, to add a new callback or change the quiet window) has nothing telling them they just broke "one coalescednodeMovedper quiet window" — the thing this whole architecture exists to guarantee. Both seams are trivially injectable already:DotNetRefis a one-method interface you can fake with an array,quietMsis a constructor arg, and vitest shipsvi.useFakeTimers(). The C# bridges got exactly this treatment (DomainChangedBridgeTests+RunChangedBridgeTestswithFakeTimeProvider) — the module's bridge is the same shape of code and Phase 0 is when the pin lands. Fix: aindex.test.tscovering — (a) deltas pushed within the window arrive as oneOnGraphChangedcall, (b)lastSelectionsuppresses 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)~
GraphCanvas.razor:28,52—ReadOnlyis consumed only atcreatetime; the module exposessetReadOnly/setNodeStatus/appendNodeTextbut the host has no wrappers. Playground never toggles it, so nothing is live-broken today — but when Phase 2 first needs a server-drivensetReadOnly, the obviousStateHasChanged-driven parameter change will silently do nothing. ASetReadOnlyAsync/SetNodeStatusAsyncwrapper (or aSetParametersAsyncoverride that pushes the diff) closes the trap before anyone falls in.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~
Buffer/Flushcalled 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! TheA_delta_from_another_run_flushes_the_buffered_run_firsttest is wonderful~AccessSecret. Hashing both sides beforeFixedTimeEqualsso 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.BeforeBuild+ResolveProjectStaticWebAssetsInputs) so a clean checkout and an incremental build both ship the module,SkipGraphEditorBuildfor 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.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 --noEmitclean, .NET suites 41/41 (3 Domain + 1 UseCases + 15 BlazorAdapter + 22 Integration, matching PR-body arithmetic exactly)(Verification notes: PR reviewed at head
e60ca18, basec5a63ac, 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. Thetoken ***ghost: my terminal's output layer masks${GH_TOKEN}in displayed text; object-DBgit grepconfirms 6 real refs, zero literal asterisks, blob SHA identical to canonical.)Addressed in
be0350c.⛔ Blockers
docker-publish.ymlconcurrency guard — addedconcurrency: { group: docker-publish-${{ github.ref }} }with nocancel-in-progress, matching CategoraiServer and nai_maker, and a comment saying why::latestis watchtower-deployed, so racing runs must queue, and a cancelled push must never leave a stale tag behind.DotNetBridge/EditorStoreuntested —src/index.test.tswith fake timers, no DOM: (a) deltas pushed within the window arrive as oneOnGraphChangedcarrying 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)lastSelectionsuppresses 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; plusnodeOpenis immediate andEditorStorenotifies on update and stops after unsubscribe. Module suite: 7 → 14 tests.💡 Ideas
GraphCanvaswrappers — taken:SetReadOnlyAsync,SetNodeStatusAsync(batchJson),AppendNodeTextAsync(nodeId, chunk); andOnParametersSetAsyncpushes a changedReadOnlyparameter to the module once the handle exists, so a re-render no longer silently changes nothing.Verification:
npm test14/14,tsc --noEmitclean, .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.🔮 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:
docker-publish.ymlconcurrency guard — present, top-level,group: docker-publish-${{ github.ref }}, queue-only (zerocancel-in-progressanywhere), 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.index.test.ts— seven tests, no DOM, fake timers, and every one traces to real semantics inindex.ts: window-restart (49 ms → nothing, 50 → one call), fresh window after flush,lastSelectiondedup + the initial-empty suppression, dispose-flush + cleared-timer-never-refires,nodeOpenoutside any window,EditorStorenotify/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)~
GraphCanvas.razor:60-66— when Phase 2's overlay bindsReadOnlylive, that's the moment for a bUnit pin (mockIJSRuntime, assertsetReadOnlyfires 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:_handlenull → skipped → create applies the value), so this is a when-it-matters nicety, not a gap — the module side ofsetReadOnlyis already pinned by theEditorStoretest.✅ What I liked~
SetNodeStatusAsync/AppendNodeTextAsyncso the run overlay's whole vocabulary exists before Phase 2 needs it._appliedReadOnlybookkeeping is exactly right — set at create, compared inOnParametersSetAsync, updated on push.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
be0350cat review time (run queued) — prior heade60ca18green 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-testforgejo-actions referenced this pull request2026-09-19 06:29:02 +02:00
forgejo-actions referenced this pull request2026-10-05 19:31:40 +02:00