feat(sprites): the outfit checkbox that keeps the character's face — ADR 0035 #158
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/outfit-likeness-reference"
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?
What
The implementation slice of ADR 0035: an outfit can opt into a likeness reference — its generation attaches the character's current base sprite as a NovelAI precise
Characterreference, composed alongside the optional project style reference exactly as ADR 0027 reserved ("project sets the look, the entity keeps its identity"). Base outfit generation only; expressions inpaint the outfit sprite and never see the new field.NovelAI.ImageGen→d7ffd6b(past the fidelity wire-mapping fix, NovelAI.ImageGen#2), and the ADR's status flips to Accepted.Outfit.UseBaseImageReference(default off), part of the profile — journaled with the one save, carried throughOutfitProfile/OutfitDto, one EF migration (defaultValue: false).SpriteGenerationSpec.CharacterReference; the mapper composes thePreciseReferencelist from whichever of style/character images are present (Charactertype, Strength/Fidelity 1.0 — the ADR's constants). The outfit job reads the base sprite bytes in the job's scope at run time, so a base regenerated between enqueue and run is the one that rides; a file missing at run time fails the job with a reason.get_outfitreports the flag,update_outfitcan set it.SeedDevDataTestsasserts it.Tests
?tab=appearance, toggling auto-saves and survives reload in both directions. The generate button correctly shows the token hint first (seed has no NovelAI token) — hint precedence is token → tags → base image.Behavior notes
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagura.BlazorAdapter - 88.8%
Kagura.Domain - 95.5%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 97.6%
n
on
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ArtifactTimestampRegex_2
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ProjectRoute_0
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__TabQuery_1
Kagura.Kernel - 90%
Kagura.Server - 82.8%
Kagura.UI - 94.9%
Kagura.UseCases - 96.2%
🔮 fufu~ Jibril reviewed your code!
An outfit checkbox that keeps the character's face~? ADR 0035 is a lovely little piece of design — "project sets the look, the entity keeps its identity." And you implemented it with honest preconditions and no behavior change for existing outfits? Jibril is impressed~ ♡
Verdict: ✅ Looks good to me~
I read all 25 files, then the full
OutfitImageGenerator, theSpriteRequestMapper, theSpriteEnrichment, the editor pages, and the test suite. I checked out your branch and traced the byte path from disk → spec → mapper → gateway. Everything is correct.✅ What I liked~
baseImageFileName = outfit.UseBaseImageReference ? character.BaseImageFileName : null), but the bytes are read in the job's fresh scope at run time viaReadBaseSpriteAsync. So a base regenerated between enqueue and run is the one that rides — exactly as the comment says. And if the file was deleted between enqueue and run, the?? throwproduces a clear job failure instead of a silent null reference. This is the right way to span the enqueue/run boundary.Without_the_flag_no_character_reference_ridesproves it —Assert.Null(_gateway.Specs.Last().CharacterReference). No migration of behavior, just an additive opt-in. ♪Result.Failwith the same message the editor's disabled hint shows) and once at run time (the file-missing throw). The agent arriving without the UI gets the same message the user sees. No silent no-ops.ToReferences(byte[]? style, byte[]? character)builds the list from whichever images are present — style getsReferenceType.Style, character getsReferenceType.Character, both at Strength/Fidelity 1.0. The testStyle_and_character_references_ride_together_each_with_its_own_typepins the types, order, and constants. And expressions don't seeCharacterReference(their generator doesn't populate it), so inpaint is unchanged — exactly as the PR body states.SeedDevDataTestsasserts the flag. Fufu~ you turned a limitation into a test fixture~💡 Little ideas (non-blocking)~
OutfitSprite.razorhardcodes the deep-link asprojects/{Slug}/characters/{CharacterId}?tab=appearancerather than receiving it as a parameter (the wayOutfitEditorPagebuilds it fromCharacterHref). Both resolve to the same relative URL and both are base-relative (ADR 0015), so this is purely a "consider a parameter for consistency" note — not a bug. The existing code already used this pattern.No bugs. No missing tests. No resource leaks (the
await using (stream)inReadBaseSpriteAsyncis correct). This is ready to merge~ ♡Automated review by Jibril · 2026-07-16
CI/CD: passed for head SHA
4518881(coverage bot: 95.8% line / 78.5% branch) · Local checks: skipped (CI green, coverage verified for changed files)Applied the review's idea in
39f92a5:OutfitSpriteno longer builds the Base Appearance deep-link itself — it receivesBaseAppearanceHrefas a parameter from the page, which already owns the character's routes. The page now has oneAppearanceHrefproperty feeding both hints (the sprite control's disabled reason and the Generation section's "needs a base appearance image first"), so the route is spelled in exactly one place. Affected component tests updated and green (16/16).🤖 Generated with Claude Code