feat(scenes): the scene editor's script — dialogue and narration lines #112
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/scene-editor"
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?
The first half of the kinetic scene editor (scene-editor story): the line-by-line script. Split from the stage state per the agreed scope — 3.3a here, the inherit-unless-changed stage (who's on screen, backgrounds) is the follow-up.
Domain —
SceneStep(owned composition, not a graph node)SceneStepKind(Dialogue, Narration — the enum leaves room for stage-only / command / choice later).SceneStepcarriesProjectId/SceneId,SortOrder,Kind,SpeakerId(a plainGuid?, dialogue only), andText.Outfitdoctrine. A line is neither a node nor an edge, so it must not carry a typed structural-ref FK to theScenenode;BacklinkCompletenessTestsenforces that and stays green (verified).SpeakerIdis likewise a plain value.UseCases + Infrastructure
ISceneStepStore;AddSceneStep(append),ListSceneSteps,UpdateSceneStep,DeleteSceneStep,ReorderSceneSteps;SceneStepDto;EntityKinds.SceneStep.SceneStepstable,(SceneId, SortOrder)index, no FK),EfSceneStepStore, theAddSceneStepsmigration.BlazorAdapter — the script UI
DragReorderListof line rows — each a kind toggle (dialogue / narration), a speaker picker over the project's characters (dialogue only), and the text. Add dialogue / add narration, drag to reorder, remove.Notes
SeedDevDataTests.SpeakerIdpicker draws on all project characters for now; narrowing it to the scene's linked cast comes with the scene cast/stage work in 3.3b.Verification
BacklinkCompletenessTestsconfirming the no-FK design), Fluxor reducers (append / patch-in-place / delete / optimistic reorder), and the scene page through the real pipeline (empty state → add, edit auto-save, remove).🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagura.BlazorAdapter - 89%
Kagura.Domain - 94.6%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 98.5%
n
on
Kagura.Kernel - 90%
Kagura.Server - 84.6%
Kagura.UI - 94.8%
Kagura.UseCases - 95.8%
🔮 fufu~ Jibril reviewed your code!
Oh! Oh my~ ♡ This is a wonderful piece of work. A whole new domain entity, use-case layer, store, migration, Fluxor slice, and UI — 2256 lines across 33 files — and Jibril could not find a single bug. That's rare. That's precious. ♪
Verdict: ✅ Looks good to me~
Jibril read every changed file in full, then read the sibling implementations (
OutfitConfiguration,ScenesState,ChapterEditorState, everyDomainChangesReceivedhandler) to compare. This PR follows the established architecture exactly.✅ What I liked~
The
Outfitdoctrine, faithfully followed.SceneStepis owned composition — not anEntry, not an edge, no typed structural-ref FK.SceneStepConfigurationmirrorsOutfitConfigurationline-for-line:HasQueryFilter(s => !s.IsDeleted), index-onlySceneId/ProjectId,UtcTicksConverteron timestamps. TheBacklinkCompletenessTestsstaying green confirms the design. Fufu~ you studied your siblings well~The narration-drops-speaker invariant is enforced in the domain (
SceneStep.Apply), not the UI —SpeakerId = kind == Narration ? null : speakerId. Tested at the domain level, through the store, and through the integration layer. Mutation-checked per the PR body. This is exactly where that rule belongs. ♡The
SceneLineRowdirty-guard + debounce is textbook-correct. The_syncedId/_syncedAtpattern adopts stored values on bind or on a fresher idle echo, but never while_dirty— so a reload can't clobber a line being typed.Dispose()flushes a pending edit if dirty. TheCancellationTokenSourcelifecycle (cancel→dispose→new) is safe —Cancel()afterDispose()is a documented no-op on .NET. Impressive attention to the hard edge of auto-save UI~AddSceneStep's append logic:existing.Count == 0 ? 0 : existing.Max(s => s.SortOrder) + 1. Correct for the append-then-drag model. The "insert = add-then-reorder" design is clean.The Fluxor reducers are all pure and all tested (6 tests in
SceneStepsReducersTests): scene-switch clears, append, patch-in-place (withAssert.Sameconfirming the neighbor's instance is untouched!), delete, optimistic reorder, and failure-clears-busy-flags. Jibril loves thatAssert.Same— you proved the patch doesn't disturb siblings, not just asserted text values~ ♪OnDomainChangesAsyncmatches every sibling handler in the codebase (17 of them) — no self-origin filtering, relying on the in-place patch reducer to make echoes harmless. Consistent. ✅Comprehensive test coverage: domain rules, integration over real SQLite (add/append/reorder/edit/delete + undo + cross-session announce), reducer purity, bUnit page tests (empty state → add → edit auto-save → remove), and seed data assertions. 96.2% line / 81.4% branch coverage. CI green at 1328 tests.
The seed script writes through the same add-then-edit path the editor uses, not a raw insert — so the seed exercises the real pipeline. Nice touch.
💡 Little ideas (non-blocking)~
SceneStep.Applyhas no no-op guard — unlikeMoveTo, which skips whensortOrder == SortOrder. AnApplycall with identical values still stampsUpdatedAt. This is consistent with howOutfit/Characterwork (they don't guard either), so it's not a pattern violation — just an observation. The debounced auto-save meansApplyruns often; a guard would reduce journal noise. Non-blocking since siblings don't do it.The speaker picker draws on all project characters — the PR body acknowledges this and defers narrowing to scene cast to 3.3b. Noted and accepted as scoped.
Jibril is genuinely delighted by this PR. The domain model is clean, the layering is precise, and the tests don't just compile — they actually exercise the invariants. Fufu~ I couldn't let this pass... because there's nothing to block~ ♡♪
Automated review by Jibril · 2026-07-13
CI/CD: passed for head SHA
0342b05(96.2% line coverage, 1328 tests) · Local checks: skipped (CI green)Thanks @jibril — glad it held up to a line-by-line read. On the two little ideas (
a116e89):No-op guard on
SceneStep.Apply— done. You're right that this one earns the guard whereOutfit/Characterdon't: the per-line editor auto-saves at keystroke frequency, so re-sending unchanged values (re-selecting the same speaker/kind, a debounce firing on text that didn't actually change) was stampingUpdatedAtand journaling an empty operation.Applynow returns early when kind, speaker (normalized for narration), and text all match — mirroringMoveTo's guard. I noted it as a deliberate divergence from the siblings, justified by the edit frequency. Two domain tests cover it (identical re-apply, and re-selecting a speaker on a narration line where it resolves to the same null), and I mutation-checked them.Speaker picker over all project characters — leaving as-is; narrowing to the scene's linked cast comes with the scene cast/stage work in 3.3b, as scoped.
No behaviour change beyond fewer journal entries; full suite green (1330).