feat(outfits): duplicate an outfit — the whole authored profile under a new name #258
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/outfit-duplicate"
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?
Duplicate an outfit into a perfect copy under a new name — the copy carries the whole authored profile (clothing tags, disabled inherited tags, inherited re-weights, likeness-reference settings) while the generated content stays behind: no sprite, no face regions, no adopted expressions. The copy starts ungenerated, exactly like a freshly created outfit.
UI (character-sprites Tier 2)
content_copyicon button (same shape as remove; hover accent, not danger). It opens a name modal ("Copy outfit") with a placeholder suggesting<source> (copy); Create/Cancel mirror the existing New-outfit modal. The copied card spins per-card viaDuplicatingId(the same one-card-spinner pattern asRemovingId).Use case
DuplicateOutfit(IOutfitStore, TimeProvider)— blank name refused ("The copy needs a name."), missing source refused ("This outfit no longer exists."), otherwise a freshGuid.CreateVersion7()record under the source's project/character withApply(sourceProfile, now)(which clamps and dedups on ingest, like every profile save). Journaled as one User operation (ADR 0020); the grid reloads through the existingOutfitsChanged+LoadOutfitsflow, so cross-session refresh rides along (ADR 0016).Assistant
duplicate_outfitmutation tool registered next to update/delete — same not-found/not-in-project guards, reports a change card routed at the copy.Submodule
external/Kagaku.UIat 3d794b2 (main with #16 merged) for thecontent_copyicon.Tests (all local, Release)
Summary
Summary
Coverage
Kagura.BlazorAdapter - 88.8%
Kagura.Domain - 94.8%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 97.7%
n
n
on
ng
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ArtifactTimestampRegex_2
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ProjectRoute_0
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__TabQuery_1
Kagura.Kernel - 90%
Kagura.Server - 82%
Kagura.UseCases - 95.7%
🔮 fufu~ Jibril reviewed your code!
Oh~ an outfit duplication slice! flips through the diff with sparkles in her eyes The profile rides along, the generated content stays behind, and nobody had to write a bespoke clone method to do it. This is a lovely little piece of architecture~ ♡
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
duplicate_outfitomits the route. Both same-file siblings hand the model a navigable handle for the record they just made:create_outfit→new { blank.Id, p.Title, Route = route }(:117),update_outfit→new { outfit.Id, outfit.Title, Route = route }(:191) — and the wider tool family does the same (20+Changedpayloads across CharacterWrite/LocationWrite/LoreWrite/StoryWrite/CgVariation…). Here the change card gets its route (third argument, :229 — the humans are happy ♡), butChangedserializes the payload as the tool result the model reads — and for a create-class tool pointing at a brand-new record, that route handle matters most: the model's follow-ups ("open the copy", "now generate its sprite") cite it. The copy exists but the model can't say where it lives. Fufu~ you wouldn't leave the new record unaddressable, would you? ♡Fix: hoist
var route = Route("characters", copy.CharacterId, "outfits", copy.Id);and returnnew { copy.Id, copy.Title, Route = route }.✅ What I liked~
Apply(sourceProfile, now)— reusing the one validated ingest path means the copy gets clamping and dedup for free, exactly like every profile save. The right shape, no shortcuts~Assert.NotEqual(0.65m, source.ReferenceStrength)before asserting the tuning carries — the seeded data can't silently drift into making the carry-proof vacuous. Oh? Oh! An anti-tautology guard before I even had to ask for one. I'm delighted ♡Guid.CreateVersion7()— the copy starts ungenerated by construction (ImageFileName/FaceRegions/FaceRegionStaleare only ever set by the generation paths), not by enumerating exclusions field-by-field. Structurally impossible to leak a sprite~OnFailedclearing every busy flag — and the test that pins all three spinners at once, comment updated to match. Meticulous ♪Origins.Userscope in the effect,InAgentOperationAsyncin the tool; the create/delete twins exactly (ADR 0020), and the effect test pins the scope.One non-issue I checked so you don't have to:
Copy()nulls_copyingbefore dispatching and leaves_copyTitlebehind — harmless, sinceAskCopyre-seeds both on open and the confirm is unreachable while closed.Automated review by Jibril · 2026-09-01
CI/CD: absent for head SHA
fe580e7(no coverage-bot comment at review time) · Local checks: build 0 warnings/0 errors (.NET 10, Release); UseCases 333/333, BlazorAdapter 850/850, Integration 736/736 — all green, matching the PR bodyReview feedback (Jibril) addressed in
745608a— thank you!1. duplicate_outfit payload omits the route — fixed. The route is now hoisted (
var route = Route("characters", copy.CharacterId, "outfits", copy.Id)) and the model-facing payload returnsnew { copy.Id, copy.Title, Route = route }, matching create_outfit/update_outfit and the wider Changed-payload family. The change-card route was already present and is unchanged.Pinned against regression: the assistant tool test now asserts the exact route string (
projects/sample-story/characters/{aoi}/outfits/{copy}) on the tool result's payload, not just the card — the model-facing side Jibril flagged. Test run: 1/1 green (the full Integration suite was green atfe580e7; this commit only touches the tool payload + that test's assertion).🔮 fufu~ Jibril reviewed your code!
Back so soon~? spins in her chair, delighted One commit, two files, and the little route handle finds its way home. That's exactly the kind of answer I like best — surgical, pinned, and done~ ♡
Verdict: ✅ Looks good to me~
The round-1 blocker is closed, precisely as asked:
var route = Route(...)hoisted, payload nownew { copy.Id, copy.Title, Route = route }. Byte-for-byte thecreate_outfit(:112/:117) andupdate_outfit(:186/:191) shape; the ternary flattened into an earlyFailurereturn matches the failure arms of both siblings too. The model gets its navigable handle to the brand-new copy ♪Assert.Equal($"projects/sample-story/characters/{aoi.Id}/outfits/{copyId}", result.GetProperty("route").GetString())— an exact-string assertion, stronger than the sibling scene test'sContains, and I tracedRoute()+ the camelCaseResultJsonpolicy to confirm the expected format is derived, not guessed. Then I did the thing I do~: deletedRoute = routefrom the payload in my review clone → the test goes red withKeyNotFoundExceptionat exactlyGetProperty("route")(:322), restored byte-identical → green. Not a tautology, and now the whole family can't silently regress ♡Everything else stands from round 1 (unchanged files):
Apply(sourceProfile, now)reusing the one validated ingest path, ungenerated-by-construction via freshGuid.CreateVersion7(), the anti-tautologyNotEqual(0.65m, ...)seed guard,OnFailedclearing all three busy flags, journaling viaOrigins.User/InAgentOperationAsync— all still lovely.Automated review by Jibril · 2026-09-01
CI/CD: stale for head
745608a(coverage bot's comment predates the push) · Local checks: build 0 warnings/0 errors (.NET 10, Release); UseCases 333/333, Integration 736/736, BlazorAdapter 849/850 — the one red isAssistantPanelTests.A_navigation_event_changes_the_route, a bUnit wait-timeout under sandbox load that passes 1/1 in isolation and touches nothing this PR changed; the pinned duplicate test green both filtered and in-suite. Mutation probe red→green as above.