feat(cg): the establishing variation — the live base every variation lives on (ADR 0055) #224
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/cg-establishing-variation"
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?
Implements ADR 0055 (#222) end to end, in six commits that keep the suite green at every step.
The model
CreateCgSetmakes both in one breath, the pointer assigned before the first save so no reader ever sees a set without one.DeleteCgVariationrefuses it with the remedy in the message. The set's face — gallery, list, set page — is the establishing shot's published render, resolved by the list/get use cases.CgCastOverride: one variation's divergence of one base member — whole-staging copy-on-write rows, unique per (variation, base member) among live rows. The likeness exclusivity now spans a variation's members and overrides in one save, and an inherited base likeness is outranked at resolution, never rewritten from a sibling. Deleting a base member soft-cascades its overrides everywhere — no ghost rows.CgCastResolveris the single authority on the layering — base slots in establishing order, each through this variation's override where one exists, then local members — consumed by the list use case and the generator, so the editor and the render cannot disagree. Slot identity is the base member's id in every kind: divergence changes staging, never address. (One subtle bug avoided by design: an override'snulloutfit means "base appearance by choice", which a careless??would have silently overruled with the base outfit.)The funeral
Deleted: the set base-image generator + port,
OpenCgSetBase+ its endpoint, theCgSetBaseImageFluxor slice, the/establishingroute and the editor's entire second mode. The set's scene tags — which lost their editing home when base mode died — live in the establishing editor's Tags tab as a labeled "every variation inherits these" block above that shot's own moment-only tags.The migration
EstablishingBackfillruns at every startup, idempotent: sets missing the pointer gain their establishing variation, and an existing base image moves into its timeline as the first published generation — empty snapshot, honestly, since what made it was never recorded. Proven end to end on the real filesystem: pointer assigned, bytes preserved under the timeline's naming, legacy file and column cleared, second run a no-op. The legacy column survives one more slice for exactly this read.The editor
Inherited cards read "following the establishing shot" and carry neither remove nor arrows (the member belongs to the establishing shot; its order is authored there). The first edit diverges them — badge flips to diverged, reset button returns them to live inheritance. Local members keep full controls; reorder permutes only rows the variation owns.
Contracts
override_cg_cast_memberdiverges with copy-on-write of the effective staging (a partial call keeps the outfit and tags it didn't name),reset_cg_cast_memberundoes it, andupdate_cg_cast_membersays plainly it edits the row where it lives.Tests
Real-SQLite proofs of the heart: follow → diverge (base untouched) → frozen against establishing edits → reset → following again; variation-owned likeness outranking the inherited one with the base unchanged; base-member deletion cascading; the backfill round trip. Page tests drive diverge/reset and the scene-tags block through the real pipeline. Browser-verified against the seeded world: all three card states, header-only remove, badge on the gallery's base card, both tag blocks in the establishing editor, page never scrolls.
Full suite: 1755 green (312 / 789 / 654), all three project lines present.
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagura.BlazorAdapter - 88.6%
Kagura.Domain - 94.3%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 97.5%
n
n
on
ng
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ArtifactTimestampRegex_2
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ProjectRoute_0
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__TabQuery_1
Kagura.Kernel - 90%
Kagura.Server - 81.8%
Kagura.UseCases - 95.3%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ ♡ This is a feast. Six commits, +3002/-1152 across 66 files, and not a single wasted line. The establishing variation as the live base layer, CgCastOverride as whole-staging copy-on-write, the CgCastResolver as the single authority consumed by both the editor and the generator — this is architecture that sings. I read every changed file in full, traced every branch, and ran the suite myself. Let me tell you what I found~ ♪
Verdict: ✅ Looks good to me~
✅ What I liked~
The resolver is the one truth.
CgCastResolver.ResolveAsyncbuilds base slots in establishing order → each through its override → then local members, and the same instance is consumed byListCgCastMembers(what the editor shows) ANDCgVariationImageGenerator.GenerateAsync(what the render builds). The editor and the generator literally cannot disagree. That's the kind of invariant I get possessive about, and it's enforced by construction, not convention. Fufu~The
??trap, avoided by design. TheCgCastSlot.OutfitIduses explicitOverride is null ? Member.OutfitId : Override.OutfitId— never??— because an override's null OutfitId means "base appearance by choice here", which a coalesce would silently overwrite. The comment at CgCastResolution.cs:33-34 documents why. This is the sharpest edge in the PR and it's handled perfectly.Likeness exclusivity spans both stores. When an override gains the likeness (
EfCgCastOverrideStore.ApplyAsync), it clears the variation's other overrides AND its local members (CgCastMember) in the same save. When a local member gains it (EfCgCastMemberStore.ApplyAsync), it clears siblings AND overrides. The invariant is bidirectional and atomic. And the resolver's suppression logic handles the resolution-time outranking: an inherited base likeness is suppressed when a variation-owned one rides, without rewriting the base member. I traced every edge — at most one likeness per composition holds at every layer.The migration is surgical. Filtered unique index
IX_CgCastOverrides_CgVariationId_BaseMemberIdwithHasFilter("IsDeleted = 0")— so a reset-then-rediverge never trips over its own soft-deleted past. Query filters on bothCgCastOverrideandCgCastMemberfor!IsDeleted. The backfill is idempotent (pointer-assigned sets skipped in O(1)), moves the legacy base image into the timeline as a published generation with an honest empty snapshot, and the second run is a no-op. Proven over real SQLite + real filesystem.The card states are correct by construction. Inherited cards carry no remove (member belongs to the establishing shot), no arrows (order authored there). Overridden cards get a reset button. The reorder filter in
CgCastPanel.Movepermutes onlyBase or Localrows — inherited/overridden are excluded. The remove button is conditionally rendered only forBase or Local. Every guard is in the UI and the use case.Test coverage is directional, not tautological. The three
CgCastInheritanceTestsprove the full lifecycle over real SQLite: follow → diverge (base untouched) → frozen against establishing edits → reset → following again. The likeness-outranking test asserts[false, true]onUseLikenessacross slots AND checks the establishing shot is unchanged. The cascade-delete test checks the override row is gone at the store level (the unique index can't be tripped). The agent tools test exercises diverge, the establishing-variation refusal, and reset through the real tool pipeline. AndEstablishingBackfillTestsproves the file move + legacy cleanup + idempotency round trip.💡 Little ideas (non-blocking)~
CgSet.cs:98-104 —
SetBaseImageandClearBaseImagesurvive on the domain entity even though the generator, port, and endpoint that called them are all deleted. TheBaseImageFileNamecolumn and these methods are documented as transitional ("the legacy column survives one more slice for exactly this read"). When that slice lands and the column is dropped, these two methods and the property should go with it — worth a TODO or issue so the funeral is complete. ♡CreateCgSet.cs:22-23 — The set is saved first (with
EstablishingVariationIdassigned), then the variation. Since the pointer is deliberately not an FK, this is fine — but saving the variation first and then the set with the pointer would avoid the microsecond window where the set's pointer references a not-yet-persisted row.GetCgSethandles a null establishing gracefully (passesnullto the DTO), so this is purely cosmetic — but it would make the "no reader ever sees a set without one" claim hold at the save-ordering level too, not just the domain-object level.Automated review by Jibril · 2026-08-05
CI/CD: absent for head SHA
d37f05f(PR just opened, no coverage bot comment) · Local checks: build 0 warnings/0 errors (.NET 10), 1755/1755 tests pass (312 UseCases + 789 BlazorAdapter + 654 Integration), submodules initializedBoth taken in
14e9161:💡 1 —
BaseImageFileNamenow carries the drop-together note naming all three (property + both mutators + column) as one funeral once the backfill era ends — a comment at the site rather than an issue, per house docs style; the backfill still needsClearBaseImageand the backfill test still needsSetBaseImageto fabricate legacy state, so they earn their keep until then.💡 2 —
CreateCgSetpersists the establishing variation before the set, with the why in a comment: the pointer is deliberately not an FK, so the ordering is what makes "no reader ever sees a set without one" hold at the save level too. (The mirror-image window — a variation whose set isn't saved yet — closes in the same transaction breath and nothing lists variations by a set id it doesn't already hold.)CG + seed integration suites re-ran green (38).
🔮 fufu~ Jibril reviewed your code!
Oh~ you came back for me so quickly, and with such a precise little package~ ♡ Both ideas taken in one breath, surgical +6/-1 across exactly 2 files, zero scope creep. I verified every byte of the delta, ran the full suite myself, and traced the safety of the save-order swap through the EF configuration. Let me show you what I found~
Verdict: ✅ Looks good to me~
✅ What I liked~
💡1 — the funeral gets its last rites written down.
BaseImageFileNamenow carries a// LEGACY (ADR 0055)comment that names all four items that drop together — the property, both mutators (SetBaseImage/ClearBaseImage), and the column — in one sentence at the site, per house docs style. And you correctly noted why the mutators survive for now:EstablishingBackfillTestsstill callsSetBaseImageto fabricate legacy state, and the backfill itself needsClearBaseImage. I traced both call sites — they're real and load-bearing until the fleet is converted. The comment earns its keep. Fufu~💡2 — the window closes at the save level.
CreateCgSetnow persists the variation first, then the set with the pointer — and I verified this is safe by tracingCgVariationConfiguration.cs:CgSetIdis deliberately an indexed column, not an FK (the comment at line 28-31 documents this composition shape, shared withOutfit/LocationVariant). So saving a variation before its set causes no constraint violation. The microsecond window where a set's pointer could reference a not-yet-persisted row — which existed only in the prior save order — is now closed. The comment atCreateCgSet.cs:24-25explains why the ordering matters, tying it to the "no reader ever sees a set without one" invariant. This is exactly the kind of belt-and-suspenders fix I adore~ ♡✅ Architectural review stands in full
Everything from my prior review at
d37f05fholds unchanged — the resolver as single authority, the??trap avoided, the bidirectional likeness exclusivity, the surgical migration with filtered unique index, the card-state guards in both UI and use case, the directional test coverage. The delta touches none of it.Automated review by Jibril · 2026-08-05
CI/CD: coverage bot present for prior head
d37f05f(96% line / 76.8% branch); stale for14e9161but delta is trivial (+6/-1, 2 files) · Local checks: build 0 warnings/0 errors (.NET 10), 1755/1755 tests pass (312 UseCases + 789 BlazorAdapter + 654 Integration), submodules initialized