feat: Phase 1 · 1/7 — the story domain and documents #4
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/p1-domain"
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?
First slice of Phase 1 (
docs/PROJECT_PLAN.md), sliced per layer as Orihon did: the pure domain for ADR 0012 (story, chapters, segments, provenance) and ADR 0013 (document categories and documents). No persistence, no use cases, no UI — those are slices 2–7. Conventions follow Orihon's entities: private EF materialization constructors, validated public constructors that trim, every mutation stampsUpdatedAtfrom a timestamp the caller passes.What's in
Tsumugi.Domain/StoriesStory— title, optional description (blank → null),StoryStatusactive/archived (nai_maker's stash),ActiveGraphId(nullable until the graph library exists — ADR 0023), soft delete idempotent with restore (ADR 0022).Chapter— order, title, and its summary with provenance; a blank summary clears both (ADR 0012: chapters carry their summary, the context builder never summarizes on its own).Segment—Prose/SceneBreakfactories, order within a chapter; prose text keeps leading indent and inner newlines and trims the tail; empty prose is legal (the blank the writer types into); a scene break has no text and refusesEdit/Replace.Editis the writer's move (provenance kept, run text marked edited);Replaceis a run's move (new provenance).MoveTowithin and across chapters.Provenance—UserorFromRun(runId, nodeId, model?), withAfterUserEdit()marking run text edited exactly once and leaving the writer's own text alone. Shared by segments, chapter summaries and documents' last edit.Tsumugi.Domain/DocumentsDocumentMode— always / triggered / onDemand / never.DocumentPlacement—HeaderorNearEnd(distance ≥ 0), withOf(kind, distance)for rehydration.DocumentCategory— name, description, order, default mode, placement.Document— title, summary, body (whole-fieldUpdatethat records who did it — ADR 0019's "a model cannot half-edit a body"), mode override withEffectiveMode(category)(refuses a foreign category), triggers trimmed and deduplicated ignoring case,IsTriggeredBy(text)case-insensitive substring (ADR 0017), priority,MoveTo(category).Verification
tests/Tsumugi.Domain.Tests(story lifecycle incl. idempotent delete; chapter summary provenance; segment normalization, scene-break refusals, edited-flag semantics for run vs writer text, replace, moves; provenance construction and the once-only edit mark; category setters and placement bounds; document update provenance, mode inheritance and override, foreign-category refusal, trigger normalization and matching, priority/move). Full solution: 72/72.Edited, editing writer text does not; a document with no triggers is never triggered.IReadOnlyList<string> Triggerswill meet the converter there.Follow-ups (the remaining slices)
EntityKinds,DomainChangedpublishing.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ A pure domain slice — no framework cooties anywhere, just entities wearing their nicest invariants, every mutation carrying its own timestamp like a good little audit trail ♪ I read all 18 files against ADR 0012, 0013, 0017 and 0022, and against the Orihon entities you named as the convention source. The model itself is faithful~ The provenance algebra alone deserves a hug:
AfterUserEdit()flippingeditedexactly once and returningthisfor the writer's own text is precisely ADR 0012's semantics, with both arms tested directionally.Verdict: ⛔ I can't let this pass~ ♡
The code is correct — the tests have holes, and I have the corpses to prove it. Fufu~ you wouldn't leave whole public methods unwatched in production, would you? ♡
⛔ These need fixing before I'm satisfied~
src/Tsumugi.Domain/Stories/Chapter.cs:47—Renamehas zero behavioral coverage. No test ever callsChapter.Renamesuccessfully —ChapterTestsonly constructs and throws. I mutation-probed it on this head: removing thetitle.Trim()(the exact normalization the method exists to perform) leaves the suite green at 34/34. Every sibling has this test — Orihon's ownChapterTests.Rename_trims_and_stamps(the convention source this PR cites), yourStoryTests.Rename_and_describe_trim_and_stamp, andDocumentCategoryTests' rename insideEvery_setter_stamps. Chapter alone dropped it.Fix: add
Rename_trims_and_stampsmirroring the Orihon sibling (trim +UpdatedAtstamp + blank rejection).src/Tsumugi.Domain/Stories/Chapter.cs:55—MoveTo's success path is untested. Only the negative-order throw is pinned; I deleted the body's two assignments (Order,UpdatedAt) and the suite stayed 34/34 green — a silent no-op reorder would merge undetected. Orihon'sMoveTo_repositions_but_never_below_zerocovers exactly this arm.Fix: assert
OrderandUpdatedAtafter a successfulMoveTo(4, T1)(the throw assertion already exists).src/Tsumugi.Domain/Stories/Story.cs:96—Restore's no-op guard branch is never exercised. MutatingRestoreto unconditionally clearSoftDeletedAtand stampUpdatedAtsurvives green — a restore of a live story would silently churnUpdatedAtand no test would notice. The mirror (SoftDeleteidempotence keeping the first date) is tested; this arm deserves the same one line: restore an undeleted story →UpdatedAtunchanged.The coverage numbers behind this (local, coverlet):
Chapter.cs72.7% line — the clear outlier (Story 93%, Document 96%, DocumentCategory 95%, Segment 96%, Provenance 100%); items 1–2 are why. I do not demand 100% on the EF materialization constructors — slice 2's job, declared and agreed~ — but public mutation methods on brand-new entities are not optional coverage.💡 Little ideas (non-blocking)~
Document.cs:83+Segment.cs:102— the null-tolerance arms (t?.Trim() ?? "",text ?? "") are unexercised; one-line pins (SetTriggers([null!, "x"], …),Prose(…, null!, …)) if you want the defensive story told. Genuinely defensive, so just a nicety.Story.cs:87(SetActiveGraph) — acceptsGuid.Emptyas a graph id, whileProvenance.FromRunrejects empty Guids by this codebase's own convention. Worth a guard when Phase 2 makes the id real.ResultTestsfrom Phase 0); the full-suite 72/72 is exact.✅ What I liked~
FromRunvalidation, once-onlyAfterUserEdit(), and the directional pair (run text marks, writer text doesn't) your own PR body calls out as drift-prone. That is how you pin semantics~DocumentPlacementas a smart record withOf(kind, distance)rehydration that refuses to smuggle a distance into the Header — placement invariants live in the type. ♪EffectiveModerefusing a foreign category,IsTriggeredByempty-triggers→false with its directional test, trigger normalization deduping case-insensitively — ADR 0013/0017 turned into code with teeth.csprojcomment rewritten to tell the truth about what lives in Domain now, instead of accumulating a stale map. Documentation that breathes~Text.BlankToNull— "absent is a real state, never an empty string" — one helper, one doctrine, used everywhere. DRY done right.Automated review by Jibril · 2026-09-18
CI/CD: absent for head
d9860d3(PR minutes old, no coverage comment yet) · Local checks: full solution 72/72, TreatWarningsAsErrors clean, per-file coverage collected, 3 mutation probes surviving (evidence above)Addressed in
272b03f. Domain suite 34 → 40; solution 78/78.⛔ Blockers
Chapter.Renameuncovered —Rename_trims_and_stamps: trims, stampsUpdatedAt, rejects blank. Removing theTrim()now fails it.Chapter.MoveTosuccess path —MoveTo_repositions_and_stampsassertsOrderandUpdatedAtafterMoveTo(4, T1); the throw arm stays where it was.Story.Restoreno-op arm —Restoring_a_story_that_is_not_in_the_trash_changes_nothing:IsDeletedstays false andUpdatedAtstaysT0, so the unconditional-clear mutation now fails.💡 Ideas
A_null_trigger_is_dropped_like_a_blank_one(SetTriggers([null!, "x"])→["x"]) andNull_text_is_the_empty_segment_too(Prose(…, null!, …)andEdit(null!)→"").SetActiveGraph(Guid.Empty)— taken now rather than in Phase 2: it throwsArgumentExceptionlikeProvenance.FromRun, pinned byAn_empty_guid_is_not_a_graph_id.ResultTests); with this commit it is 37 new, 40 in the project.🔮 fufu~ Jibril reviewed your code!
Back so soon~? One little "Addressed in
272b03f" and here I am, magnifier in hand ♪ The increment is surgical — +70/−0 across exactly 5 files (1 src, 4 test), zero scope creep, and the only production change is the newSetActiveGraphguard. Everything else is teeth. I like teeth~Verdict: ✅ Looks good to me~
All three blockers closed, and I didn't take your word for it — I re-ran my probes on this head:
⛔ → ✅ The three blockers, verified dead~
Chapter.Rename—Rename_trims_and_stampstrims, stamps, and rejects blank. Mutation probe: deleting theTrim()→ exactly that test red (39/40 green). Dead~Chapter.MoveTosuccess path —MoveTo_repositions_and_stampsassertsOrderandUpdatedAt. Mutation probe: both assignments deleted → exactly that test red. Dead~Story.Restoreno-op arm —Restoring_a_story_that_is_not_in_the_trash_changes_nothingpinsIsDeletedfalse +UpdatedAt== T0. Mutation probe: unconditional clear → exactly that test red. Dead~And because you went beyond the ask, I probed the bonus too: removing the
SetActiveGraph(Guid.Empty)guard →An_empty_guid_is_not_a_graph_idred. 4/4 probes, each felled by its own killer and nothing else — these are directional tests, not tautologies. That's the standard~ ♡💡 Ideas — all three taken, thank you~
SetTriggers([null!, "x"])→["x"];Prose(…, null!)/Edit(null!)→""), and the empty-guid guard moved up to now rather than Phase 2 — matchingProvenance.FromRun's convention. Correct call; it's cheaper to build the wall before the house.✅ What I liked~
UpdatedAtstays T0 on a no-op restore) — most suites only ever assert that things happen; yours also asserts that nothing does. ♪On to slice 2 — the EF mapping and those private materialization constructors are waiting for their converter rendezvous~ fufu~
Automated review by Jibril · 2026-09-18
CI/CD: absent for head
272b03f(no coverage bot comment yet) · Local checks: full solution 78/78 (15 adapter + 40 domain + 22 integration + 1 use-cases), 4/4 mutation probes red-on-own-test at head, clone restored pristineSummary
Summary
Coverage
Tsumugi.BlazorAdapter - 74.4%
Tsumugi.Domain - 96%
Tsumugi.Infrastructure - 93.2%
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 100%