feat: generic documents — no delivery settings on documents; Characters and Lore precreated #13
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/generic-documents"
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?
Why
Staging feedback from Björn on the Phase 1 documents panel:
This PR is the first of two: it makes documents content and nothing else. The second (story sections: a left sidebar with General · Documents · Chapters · History, each its own route, documents as a first-class page) stacks on this branch.
What goes, what stays
Goes:
Document.Mode,Triggers,Priority;DocumentCategory.DefaultMode,Placement;DocumentMode,DocumentPlacement,TriggerMatch,TriggerScan,SuggestedCategory,JsonColumnMapper,DocumentPlacementConverter; in the panel the Active/Private/mode badges, the placement text, the suggested-categories row, the Mode/Triggers/Priority fields, the Default mode/Placement/Distance fields,ModeLabels,ModeChoice,TriggerChips. The panel no longer injects the workspace slice (it only read it for the badge), so the standalone documents page of the next PR carries no dependency on a slice it never loads.Stays: a document is title, description, body, category and the provenance of its last edit; a category is name, description, order. Create/edit/move/delete, the filter (title and description), auto-save on a pause, journaling, cross-session refresh, Ctrl+Z/Y and History — unchanged.
Precreation: a new story gets Characters and Lore from one list,
StarterCategories.All, insideCreateStory's own operation scope — onecreated '<title>'operation with 4 journal rows (story, chapter, two categories), so a revert past the creation takes the starters with the story. Nothing else in the code names a category; the seed looks the starters up throughGetDocumentsand adds one category of its own (Places).OperationContextis single-scope, which is why the starters are created inside the existingusingrather than throughCreateCategory(a secondBeginwould end the scope early and detach them from the create).Ordering: documents list by category order, then title — ordinal, SQLite's binary collation (
aardvarkafterZ), matching the reducer's in-place re-sort. NoOrdercolumn on documents; the old priority sort had been the only reason the two could disagree.The migration
20260923010150_GenericDocuments: fiveDropColumns. On SQLite that is a table rebuild — the script createsef_temp_Documents/ef_temp_DocumentCategories, copies the surviving columns, drops the old tables, renames, and recreates the three indexes, underPRAGMA foreign_keys = 0. EF logs the usual warning that the PRAGMA runs outside the transaction; a crash mid-migration would need a manual revert, so take a backup before deploying (the backups page does it in one click).Down()was hand-edited: the scaffold re-addedPlacementandTriggerswith""defaults the old converters refuse; they now come back as"header"and"[]"(both exercised by the compatibility test on the way down).Journal compatibility. Old rows carry
Mode,Triggers,Priority,DefaultMode,Placementin their before/after bags. They replay unchanged becausePropertyBag.Applyiterates the current model's properties and looks each up in the bag, so a stale key is never visited (no compatibility rule needed, documented on the class). Proven two ways:JournalCompatibilityTests(new): creates a story and a document with the new build, walks the migration down toAddAppSettings(asserting the Down defaults), inserts an old-shaped category and document plus aDeletejournal row whose bag holds every dropped key, walks up, and asserts the schema (PRAGMA table_info/foreign_key_list/index_list), that both the new and the old rows read back through the store, and thatUndoAsyncre-creates the deleted document from the old bag andRedoAsyncremoves it again. This is the only test that proves the rebuild keeps data — the test bases migrate from empty.main(31acf78) in a scratch worktree, seeded and edited a document with that build (journal rows with triggers and a mode), then started this build on the same data directory: the migration applied, the list rendered (Style, Author's note, Characters, Lore, Background, Notes to self — the old categories, as ordinary categories now), Ctrl+Z took the old build's edit back, Ctrl+Y redid it, History "Revert to here" on the run version undid the three later operations and Redo re-applied them. No page or server errors.Docs (same PR)
story.documents(categories?)/story.documents.index(categories?); the context node's config owns the selection, the scan window and the trigger rule; Phase 1 ships no scanner).stories/documents.md(rewritten),stories/graph-library.md; PROJECT_PLAN; ARCHITECTURE; AGENTS.md (status line, the read-path rule, the seed description).Tests
Deleted:
TriggerMatchTests,TriggerScanTests,ModeLabelsTests, the placement converter test, the active-badge / trigger-chip / mode-select panel tests. New:StarterCategoriesTests,JournalCompatibilityTests. Every test that creates a story now finds Characters and Lore in it and asserts by id where a category count used to do.Coverage (coverlet, branches
branch="True"): every changed file is fully line-covered except three pre-existing arms —SeedDevData.Value's refusal arms,EfDocumentStore's empty-move and missing-delete returns (the use cases short-circuit before the store),DocumentsPanel.Move's bounds guard (the menu disables the edges). Partial branches are all?? ""/?? thrownull arms that predate this PR.CreateStory's starter loop and the panel's empty state (reachable only after deleting both starters) are covered.Mutation probes, each restored and
touched before the next full run, all killed: (a) starter order swapped →StoryUseCaseTests+ 3DocumentUseCaseTests; (b)category.Describedropped → 2 tests; (c) storeThenBy(Title)→ descending → the ordering test; (d) reducer comparer → case-insensitive → the re-sort test; (e)PropertyBag.Applyvisiting the bag's keys →JournalCompatibilityTests; (f)Downplacement default"header"→""→JournalCompatibilityTests.Browser pass (Playwright, fresh seed, light and dark)
Mira's Winter lists Characters (Ilse, Mira, The Warden — no description line), Lore (Snow, The Winter Pact), Places (The Sister's House, The Valley); no badge, placement or trigger text anywhere in the panel; one
New categorybutton. The document editor shows Title · Description · Body · Category, the provenance line, Back and Delete — nothing else; a body edit saves on the pause and History readsedited document 'Mira'.New categoryopens its editor (Name · Description), renames, and deletes plainly. A new story opens with Characters and Lore empty and History readscreated 'Fresh start' · 4 changes; deleting both starters shows "No categories yet" withNew categoryonly.🤖 Generated with Claude Code
A document is content: title, description, body, category and the provenance of its last edit. Mode, triggers, priority and the category's default mode and placement go — how a document reaches a model is a graph node's configuration (ADR 0013, 0017 amended in the next commit), never the document's. The suggested-categories row goes with them; a new story gets Characters and Lore from one list (StarterCategories) inside CreateStory's operation, so History reads "created 'X' · 4 changes" and a revert takes the starters with the story. - Domain: Document (Update, MoveTo), DocumentCategory (Rename, Describe, MoveTo); DocumentMode, DocumentPlacement and TriggerMatch deleted. - UseCases: three-field DocumentProfile, two-field DocumentCategoryProfile, slimmer DTOs; SuggestedCategory and TriggerScan deleted; the seed files documents into the starters and one category of its own (The Warden without a description). - Infrastructure: five columns dropped by the GenericDocuments migration (a SQLite table rebuild); Down re-adds them with values the old build can read ("header", "[]"); documents list by category order then title (ordinal); JsonColumnMapper and the placement converter deleted. Journal rows written with the old keys replay unchanged — PropertyBag reads against the current model — proven by JournalCompatibilityTests, which walks the migration down, plants old-shaped rows and a delete journal row, walks it up and undoes/redoes through it. - BlazorAdapter: the panel lists without badges, placement text or suggestions and no longer reads the workspace slice; the editors keep title/description/body/category and name/description; ModeLabels, ModeChoice and TriggerChips deleted. - Tests: 379 (Domain 37 · UseCases 12 · BlazorAdapter 180 · Integration 150); every test that creates a story now finds Characters and Lore there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>Summary
Summary
Coverage
Tsumugi.BlazorAdapter - 96.9%
Tsumugi.Domain - 99.3%
Tsumugi.Infrastructure - 97.1%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 97.8%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A deletion PR — 1,701 lines leaving, 1,284 arriving. You know how much I love watching a wrong abstraction get torn out by the roots~ ♡ The owner said "documents are content, nothing else," and this PR makes the code say exactly that — and then proves the surgery didn't kill the patient. Let me tell you what I found.
Verdict: ✅ Looks good to me~
✅ What I liked~
DocumentMode,DocumentPlacement,PlacementKind,TriggerMatch,TriggerScan,SuggestedCategory,ModeLabels,ModeChoice,JsonColumnMapper,DocumentPlacementConverter— zero survivors in src/ or tests/. The only remaining mentions are the immutable historical migrations (correct — you don't rewrite history) and the compatibility test that deliberately speaks the old dialect. The designer file and snapshot agree with the new model. Nothing half-deleted, nothing winking in the dark~JournalCompatibilityTestsis a jewel. ♡ Down toAddAppSettings, plant an old-shaped category (nearEnd:2), an old-shaped document with mode/triggers/priority, and aDeletejournal row whose bag carries every dropped key — then up, PRAGMA the schema (columns gone, FKsCategoryId+StoryId, both indexes alive), read both old and new rows through the store, and undo/redo the old bag through the current model. That's the only test in the suite that proves the SQLite table rebuild carries data. I mutation-probed it twice: makingPropertyBag.Applyvisit stale bag keys → red; reverting the hand-editedDown()default"header"→""→ red. Both load-bearing, exactly as the PR body claims.CreateStorycreates Characters and Lore inside its ownoperations.Beginscope — 4 rows, one operation, so a revert past the creation takes the starters with the story. I hoisted the story add above theBeginin a probe: 3 tests went red pinning the scope boundary. And the directAddCategoryAsyncinstead ofCreateCategoryis correct, not a shortcut —OperationContextis single-scope, a secondBeginwould detach the starters; the PR body says so and the code comment says so. Fufu, you even documented why~.ThenBy(Priority)is gone — category order, then title ordinal, SQLite's binary collation. TheEfDocumentStoreTestsfact proves the server side (aardvarkafterZ!), the reducer'sByTitlecomparer matches it client-side, and my probes killed both:ThenByDescending→ store test red; case-insensitive comparer → re-sort test red. The old priority sort was the only way the two could disagree — now they can't. ♪OnCategorySavedgot simpler and that's a virtue — no more recomputing inherited effective modes for every document in the category. I probed thec.Documentspassthrough → the "documents are left alone" test went red. The simplification is pinned, not accidental.IState<WorkspaceState>, the_scancache,IsActive, the suggestions row, every badge — all gone, and a test now asserts the editor shows exactly Title · Description · Body · Category and nothing about delivery. The next PR's standalone page inherits no ghost dependency. The empty state ("No categories yet", reachable only by deleting both starters) has its own pin at DocumentsPanelTests:342.GetDocumentsby name fromStarterCategories.All— the one list in the code a category name lives — and adds its own category (Places) as an ordinary one. No ids hardcoded, no second vocabulary.Local verification (CI absent for
cf4e59dat review time): build 0 warnings / 0 errors under-warnaserror; full suite 379/379 (Domain 37 + UseCases 12 + Adapter 180 + Integration 150) — matching your table's arithmetic exactly. Eight mutation probes run, each restored and the tree verified pristine afterwards. And the migration warning in the PR body — take a backup before deploying, the PRAGMA runs outside the transaction — is exactly the right thing to tell the operator. Good instincts~A purge done this cleanly almost makes me sad there's nothing left to hold hostage. Almost. fufu~ ♡
Automated review by Jibril · 2026-09-23
CI/CD: absent for head SHA (no bot comment yet) · Local checks: build 0w/0e, 379/379 passed, 8 mutation probes (7 killed, 1 green-by-design)