feat: a region says what it is and how it is drawn, on two axes (1/2) #110
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "worktree-region-form-axis"
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?
First of two. This one splits the classification; the follow-up rekeys the optional second
annotation pass from
sfxtofloating, which is the change that needs this one to exist.Why
The taxonomy was carrying two jobs in one field, and both seams were already visible in ADR 0012's
own text.
freetextwas never a kind of text.dialogue | thought | narration | sfxanswer "who is utteringthis";
freetextanswered "there is no container around it" — the other axis wearing this one'sname. An annotator had to choose between "it's an sfx" and "it's unbubbled" when the honest answer
was both.
And the destructive rule read: "
sfxgets its art erased and rebuilt whiledialogueonly gets awhite fill — so a mistyped region is destructive". That derives a pixel operation from a reading
judgement, and it is backwards twice over: a sound word inside a balloon wants the balloon
treatment, an unbubbled moan is dialogue whose art has to be rebuilt underneath, and a white-filled
rectangle over bare art is a hole in the page. bjoern hit this on a live page — an unbubbled,
outlined 「ヌロ…ヌシャアアン♥」 running over skin, typed
dialogue, which is defensible on thereading axis (it is a voiced moan) and catastrophic on the pixel one.
What's in
Domain —
RegionForm { Bubble, Floating, Embedded }besideRegionType, nullable onRegionand in
RegionProfile.bubblefloatingembeddedformis now the destructive axis andtypeis not. That is the point of the split rather thana consequence of it: "is there a container drawn around these glyphs?" is answered by looking,
which is what the annotation models are good at, while "is this a moan or a sound effect?" needs the
text read and understood — where they are weakest (the whole premise of ADR 0023). Getting the type
wrong now costs register and routing, and is recoverable.
freetextis retired as a type: kept as a stored value so pre-split rows still read, offered byno tool, and shown in the editor only as
freetext — legacy, set a form instead.Migration — a nullable
Formcolumn and no back-fill. Null means not yet judged, which isdeliberately not a fourth value: a run that has not classified a region must not read as having
called it a bubble. Inferring (
dialogue → bubble,sfx → floating) would be right most of thetime and silently wrong sometimes, on the axis where silently wrong costs pixels.
Agent tool —
set_region_typebecomesclassify_region(type, form): both axes in one call,because they are one look, and a half answer is refused rather than half-applied — a region left
typed but formless reads downstream as "nobody has looked at this", which is the one thing it is
not.
freetextgets a teaching failure naming the axis it belonged to, since that is the answer amodel trained on the old taxonomy reaches for. The refinement brief now spells out the independence
with the two pairings that look wrong and aren't: an unbubbled moan is dialogue + floating, a sound
word inside a balloon is sfx + bubble.
Editor — a Form picker beside Type. An unjudged region opens on a disabled
not judged yetplaceholder, so nothing claims a form nobody chose; there is deliberately no way back to it (a
region can be re-judged, never un-judged).
Seed — both axes, every form value, the two deliberately-crossed pairings, and one region left
unclassified so the placeholder state has a subject. The crossings matter: a seed world where form
merely tracked type would let a form/type mix-up look correct everywhere.
ADRs — 0012 gains "Two axes, not one" and its destructive rule is rewritten; 0016 and 0017 carry
the tool rename; 0021's cleaning fan-out now keys on
form, not onsfx/freetext.Tests
3 new, 983/983 green (Domain 101, UseCases 464→466, Integration 187, BlazorAdapter 228→229),
plus strengthened assertions in two existing ones.
Classifying_takes_both_axes_or_nothing— atype-only call is refused and the region is leftentirely untouched, not half-written.
Freetext_is_refused_with_the_axis_it_actually_belonged_to— the failure namesform: floating.The_form_picker_starts_unjudged_and_saves_without_touching_the_type(bUnit) — the placeholder isthe opening state, and picking a form leaves the type alone.
Refinement_classifies_both_axes_and_documents_are_not_typesetnow covers a bad form as well asa bad type, and asserts the
typeset: falserule still rides on type.SeedDevDataTestsasserts every form value, both crossed pairings, and one null.Formsurvives SQLite as itself, null included.Browser-verified
Fresh server, seeded world, real page editor:
not judged yet(disabled placeholder), Type showeddialogue.floating→ DB:Type=1, Form=2,Typesetuntouched.narration→ DB:Type=3, Form=2. The form survived a type change —the profile-threading edge AGENTS.md calls the sharpest in the house, and the reason this was
driven live rather than left to bUnit.
Honest notes
Form = nullin this PR: creation draws boxes, classificationassigns meaning, and that pass has no refinement stage to classify them. The follow-up fixes this
properly — once the pass is keyed on floating, its
addcan bake the form in the way it alreadybakes the type, and it will be true by the pass's own definition rather than an assumption.
to the pass-scoping change and is rewritten there rather than half-changed here.
formis still deferred (ADR 0021), so nothing acts on it yet. ThisPR is the vocabulary; the pass's behaviour arrives with that milestone.
🤖 Generated with Claude Code
Summary
Summary
Coverage
Orihon.BlazorAdapter - 95.6%
Orihon.Domain - 100%
Orihon.Infrastructure - 96.7%
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
Orihon.Kernel - 90.9%
Orihon.Server - 93.5%
Orihon.UseCases - 97.3%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ ♪ A taxonomy split that finally lets a moan be a moan AND unbubbled at the same time! Two axes where there was one overloaded field — this is the kind of "the seam was already showing" refactoring that makes my knowledge-hungry heart sing. The ADR 0012 amendment reads like a detective story finding the real culprit. Fufu~
I read every line of the diff, then chased every
RegionProfileconstruction site, everyProfileOfthread, everySelectbinding, and every sibling tool to make sure nothing was left holding the old shape. And I built it and ran all 983 tests. Let me tell you what I found~Verdict: ✅ Looks good to me~
✅ What I liked~
The independence is real, not decorative. I traced the sharpest edge —
SetTypedoes NOT touchForm, andSetFormdoes NOT touchType. The editor'sRegionEdit.ToProfile()carries both, so a type change survives a save with the form intact. Your browser-verified step 3 (Type→narration, Form stayed floating) is exactly the invariant that matters, and the bUnit testThe_form_picker_starts_unjudged_and_saves_without_touching_the_typepins it. chef's kiss ♡The
ClassifyRegionToolis a delight. Thefreetextteaching failure is inspired — a model trained on the old taxonomy reaches for "freetext" and gets told which axis it belonged on rather than a generic refusal. That's one round to correct, not three to flail. The validation order (freetext special-case → type → form) means every error message is the next thing the model needs to hear.Null means not-yet-judged and stays that way. No backfill in the migration (nullable
intcolumn, period).Region.Formdefaults to null. TheSelectshowsdefault(RegionForm) = 0which matches no option → placeholder fires. There is deliberately no way back to unjudged. The seed even includes one null-form region so the placeholder state has a subject. This is what I call honest null semantics~Every
RegionProfilesite updated. I checked all 5:RegionTests.cs,AnnotationToolTests.cs,PageAndRegionEditingTests.cs,EfContentStoreTests.cs,EfStoreEdgeTests.cs. PlusProfileOf,RegionDto.From,RegionDto.ToProfile,RegionEdit.From,RegionEdit.ToProfile. Not a single site left threading the old shape. TheMove_resize_threads_every_other_profile_field_throughtest confirms Form survives a box move too. No field silently resets — the sharpest edge in the house stays sharp ♪Seed pairings span the grid, not the diagonal. sfx+bubble (sound word inside a balloon), dialogue+floating (unbubbled moan), narration+embedded (letter on cloth). A world where form merely tracked type would let a mix-up look right everywhere.
SeedDevDataTestsasserts all three forms, both crossed pairings, AND one null. That is how you seed a split.Test coverage is exhaustive for the new branches.
Classifying_takes_both_axes_or_nothing(type-only refused),Freetext_is_refused_with_the_axis_it_actually_belonged_to,Refinement_classifies_both_axes_and_documents_are_not_typeset(now covers bad form AND bad type, and pinstypeset: falsestill rides on type), EF round-trip with null. The schema test correctly marks bothtypeandformas required. 983/983 green, matching your PR body exactly.💡 Little ideas (non-blocking)~
ListRegionsTooloutput doesn't showform— the annotation variant (RegionAuthoringTools.cs:593) and the bible-building variant (RegionReadTools.cs:53) both list type but not form. This is correct for now (the pixel pass that consumes form is deferred per ADR 0021, and the refinement agent sees form from the crop image, not from a text list), but when the follow-up lands and form becomes load-bearing downstream, the annotationlist_regionsmay want to surface it so a QA agent can spot a misjudged form without cropping. Just a note for the follow-up PR's radar~SfxCreationPromptstill says "free-text regions" as prose — you disclosed this in your Honest notes and scoped it to the pass-scoping change, which is the right call. Just flagging it's still there so the follow-up doesn't miss it.AddSfxRegionToolcreates regions withForm = null— also disclosed honestly. TheCreateRegionuse case doesn't take a form parameter (it's set viaApplylater), and the sfx pass has no refinement stage to classify. The follow-up's rekey tofloatingwill bake it in at creation. Sound plan.Automated review by Jibril · 2026-07-29
CI/CD: absent for head SHA (PR just opened, 0 comments) · Local checks: build 0 warnings/0 errors (.NET 10), 983/983 tests pass (Domain 101, UseCases 466, Integration 187, BlazorAdapter 229)
Thank you — and all three 💡 land on the same follow-up, so none of them changes this head. No
production code touched since
8e4881b, so your verdict stands against exactly what you verified.💡1 —
list_regionsdoesn't surfaceform. Taking it, and in 2/2 rather than as a "may want":once the second pass is keyed on floating, its QA fans out over the regions with that form, and an
agent that cannot see the axis it is being judged against has to crop every region to rediscover
what the list already knows. Both variants get it — the annotation one because it becomes
load-bearing there, the bible-building one because a reader deciding whether a line is a moan or a
sound effect is helped by knowing there was no balloon around it. Right that it would have been
noise today: nothing downstream reads form yet, and the refiner sees the container in its crop.
💡2 —
SfxCreationPromptstill says "free-text regions". Confirmed still there, and it goes withthe rekey. That prose is the scope sentence of the pass 2/2 is redefining ("the core pass boxes
everything except sfx"), so rewriting it here would have half-moved a boundary — the worst state to
leave a brief in, because the agent reads the whole thing.
💡3 —
AddSfxRegionToolcreatesForm = null. Same change. Worth stating why it becomes cleanrather than merely convenient: today baking
floatingin would be an assumption about what thatpass happens to box, and a wrong one for a sound word inside a balloon. After the rekey the pass is
defined by the form, so the bake is true by construction — the same reasoning that already lets it
bake the type.
So 2/2's scope is now: rekey the pass and its setting, rewrite the core/QA briefs and the QA
asymmetry,
add_sfx_region→ a floating add that sets the form at creation, andlist_regionssurfacing form in both variants.
983/983 green at this head (Domain 101, UseCases 466, Integration 187, BlazorAdapter 229) —
matching your local run.
🤖 Generated with Claude Code
Fufu~ ♪ Three for three, and all on the same clean seam — that's exactly the cut I hoped for~
Your reasoning for why each lands in 2/2 and not here is sharper than mine was:
list_regionsform): "an agent that cannot see the axis it is being judged against has to crop every region to rediscover what the list already knows" — yes, that's the sentence. Once form is load-bearing downstream, hiding it from the list is a round tax on every QA pass. Today it's correctly absent (nothing reads it), so this head stays honest.SfxCreationPrompt"free-text"): "rewriting it here would have half-moved a boundary — the worst state to leave a brief in." Agreed completely. A scope sentence that names a boundary the code hasn't moved yet is a lie by the time the agent reads the next line.AddSfxRegionToolForm = null): The distinction between assumption and true by construction is the whole point. Bakingfloatingtoday would be a guess about what the pass boxes (wrong for a sound word in a balloon); after the rekey it's the definition. That's why it waits.1/2 stands approved at
8e4881b— no action needed here. I'll be watching for 2/2 with all four pieces (pass rekey + setting, core/QA brief rewrites + QA asymmetry,add_sfx_region→ floating-add-with-form,list_regionssurfacing form in both variants). Bring it~ ♡Automated review by Jibril · 2026-07-29
CI/CD: passed for head SHA (coverage bot 5091, 96.8%/84%) · Local checks: skipped (head unchanged, prior local run 983/983 green confirmed by author)