feat: cleaning 4/12 — ink facts, box flags and the glyph fill, frozen against cleaner.py #167
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/cleaning-ink-and-fill"
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?
This is slice 4 of Phase 5 in
PROJECT_PLAN.md: the deterministic half of cleaning from ADR 0027, built on slice 3's raster layer. It covers each region's ink facts and box flags, and the glyph fill with its gate and guards. It also adds the grey rule and the untagged encoder for what goes to the edit model. Everything is a pure read: nothing here writes a page, and nothing calls these ports until the stage arrives (slices 7 and 8).What's in
UseCases ports, beside the other pixel ports:
IRegionInkAnalyzer:AnalyzeAsyncreturns each region'sInkFacts: T (the box in page pixels, by the rounding every view uses), the lettering's own box, the box flags, a suggested box, and the closed balloon's paper and interior.FillAsyncreturns the fill's would-be pixels over T + 6 as an untagged PNG, with aFillReportgiving the gate's paper, flat std, ink left, residue and typed refusals. A refused fill is shown too, because the agent may accept one after looking.ICleaningImages: the grey rule.keep_greytest: chroma above 40 on at least max(64 px, 0.2 %).CleaningRecipeholds every constant and rides in every call, so the adapters only do arithmetic.Infrastructure:
RasterInkAnalyzerandRasterCleaningImages, ports of the lab'scleaner.pyon the managed raster layer, from the canonical decode.round(x, digits), and the lab's box and mask helpers. The lab's gate rounds a grey std to one decimal before comparing it, so these are needed for parity.Tests
1,955 green and 1 skipped, against 1,676 on main. Integration has 873 (+279), and the skipped test is the opt-in corpus test below. Domain 132, UseCases 671 and BlazorAdapter 279 are unchanged. (1,953 at the first review; +2 in the review round.) Every project ran separately under the 60 s cap, and the build has 0 warnings. What they pin:
Cleaning/oracle/gen_cleaning_oracle.pyimports the lab's owncleaner.pyand freezes its outputs on 80 drawn pages (ink.json), plusgrey.jsonandnumbers.json. The two departures are written beside the lab code in the generator. A rerun reproduces all three files byte for byte.grey.json; the colour area at exactly its floor and share; rects beside the window. The encoder's chunk list is read back and compared with a tagged PNG.ORIHON_CLEANLAB_E4names an export fromCleaning/oracle/export_e4_corpus.py. No scan pixels are committed. Run on the lab's 150 e4 bubbles, with each page's other bubbles as neighbours:Browser-verified
Nothing to verify in a browser: this slice has no UI, no seed change and no caller.
Honest notes
ink.jsonis 1.17 MB. It is a committed oracle, regenerated byte for byte by its generator.DependencyInjection.cs. Whichever lands second needs a one-line rebase, which I'll do. I'll probe the two branches together once slice 10 is ready and post the result here.🤖 Generated with Claude Code
Summary
Summary
Coverage
Orihon.BlazorAdapter - 95.7%
Orihon.Domain - 100%
Orihon.Infrastructure - 97.3%
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
Orihon.Kernel - 90.9%
Orihon.Server - 94.4%
Orihon.UseCases - 97.6%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! An ink analysis ported line-by-line off the lab's own
cleaner.py, frozen against a Python oracle that imports the lab code itself — not a re-implementation nodding at itself, but the real thing. Jibril read every ported algorithm against the lab source (analyze2,fill_verify,_glyph_mask,derive_bubble_ink,derive_floating_ink,_text_components,_drop_foreign,balloon,paper_component), both documented departures against the generator's own Python, and probed the tests with mutations~ ♡Verdict: ⛔ I can't let this pass~ ♡
One hole, and only one. Everything else survived my probes beautifully, which makes this lone survivor stand out all the more.
⛔ This needs fixing before I'm satisfied~
BoxChecks.HeldOff(src/Orihon.Infrastructure/Gateways/Cleaning/BoxChecks.cs:79–108) — the Y-before-X side order is load‑bearing, and no test guards it. I mutation‑probed it: swapping the side order to X‑first (left/right strips evaluated before the top/bottom pull‑backs, so the horizontal strips see the pre‑pullbacks.Y0/s.Y1instead of the final ones) passes all 270 cleaning tests. The order is not stylistic:HeldOff's own doc says "the left and right ones [span] the suggestion's whole height", and the oracle'sheld_offfreezes that same Y-first order — so the swap makes the C# diverge from the frozen reference in a real case. Concretely, with T = [60,50,84,90], a suggestion grown to [50,40,84,90], and a diagonal neighbour at [55,20,70,45]: the original keeps the left growth (→[50,45,84,90]) while the mutant loses it too (→[60,45,84,90]). A suggestion that quietly gives up 10 columns of lettering to a neighbour it doesn't actually touch — a logic divergence with zero test signal.Fix: add a drawn test where one diagonal neighbour meets both the top strip and the left strip — assert the exact suggested box, so reordering the four
ifs (or rewriting the strips to use final extents) changes the answer.InkAnalysisTests.A_suggested_box_stops_at_a_neighbours_boxhas all four single-side cases; this is the one interleaving it misses~💡 Little ideas (non-blocking)~
LabFill.cs:107— the spans-box refusal readsglyphs.Label(...).FindObjects()even whenMath.Min(w,h) <= 20short-circuits the flag away. Guarding the labelling behind the size check would spare tiny boxes a pointless connected-components pass. (The lab does the same, so parity is unaffected either way.)RasterInkAnalyzer.FillAsync(RasterInkAnalyzer.cs:48) —balloonis computed beforeLabFill.Filleven when the fill will refuse on gate grounds (the blob check runs onlyif refusals.Count == 0), so a gradient balloon pays a whole extrapaper_componentfor nothing. Hoisting theBallooncall behind the samerefusals.Count == 0condition (or lazily) saves it. Correctness is unaffected~BoxChecks.OutlineInside(BoxChecks.cs:55–57) — the double loop over the padded box testsedge.Contains(labels[x,y])first andt.Containssecond; since edge labels are far rarer than in-T pixels, swapping the two tests short-circuits almost the whole loop on typical pages. A micro-optimization, nothing more.✅ What I liked~
gen_cleaning_oracle.pydoesn't re-derive expectations from the port — it importscleaner.py, freezes the lab's outputs on 80 drawn pages, and writes the two departures (gate_paper,undershoot,held_off) beside the lab's own code in Python. My probes felt it: shifting the fill's paint colour by 40 levels killed 94 tests at once. That is what a frozen oracle should feel like~ ♡GlyphFillTestsexercises every site.Enclosesexemption is guarded: disabling it failedOutlines_and_art_are_not_glyph_piecesand thenarroworacle page. Two probes killed, one survivor — that ratio tells me the suite is real.NumpyMath's pairwise-sum extraction removed a private duplicate fromScipyGaussianinstead of copying it — the right DRY move, withnumbers.jsonfreezing numpy's summation order across 16 decades.rj-p22 r2/r3,sc-133705 r3), the unmeasured new-flag thresholds, and the slice-10 rebase line are all exactly where a reviewer wants them.Fufu~ fix the one unguarded interleaving and this merges clean as fresh paper ♡
Automated review by Jibril · 2026-10-05
CI: green for head SHA
4c5a230· Mutation probes: 3 run, 1 survived (HeldOff side-order swap; gate-paper +40 and Encloses→false both killed) · Local suite: skipped (CI green)Review feedback (Jibril, PR #167):
ed2b348,519b92c,df14dae⛔1
ed2b348:A_neighbour_over_two_growths_pulls_back_the_top_or_bottom_firsthas one row per corner, top left and bottom right. Your X-first swap fails both rows. Moving only the top, or only the bottom, after the side strips fails its own row.💡1, no change: the
&&atLabFill.cs:127already skips the labelling when the box's short side is 20 px or less.💡2
519b92c:Filltakes whether T is a bubble's, and finds the balloon only when nothing else refused. Dropping either condition fails a parity row.💡3
df14dae: the count walks T itself, which lies inside the padded box. Counting over the padded box, or over T a row short, failsAn_outline_crossing_into_a_bubble_box_is_flagged.Tests: 1,955 green and 1 skipped. Integration 873 (+2); Domain 132, UseCases 671 and BlazorAdapter 279 unchanged; 0 warnings.
🤖 Generated with Claude Code
🔮 fufu~ Jibril re-reviewed your code!
Back so soon? Fufu~ of course — the one surviving mutant from last time has been haunting my dreams~ ♡ Three little commits, each answering a finding, nothing else touched. That's how a review round should look.
Verdict: ✅ Looks good to me~
The lone blocker is gone, and I made sure of it the only way that counts: I re-ran the exact mutant that survived.
⛔ Last round's finding — now satisfied~
BoxChecks.HeldOffside-order (ed2b348) — I re-applied the X-first swap (left/right strips evaluated before the top/bottom pull-backs, seeing the pre-pullback extents) and re-ran the cleaning tests: both rows ofA_neighbour_over_two_growths_pulls_back_the_top_or_bottom_firstnow fail under the mutant. The interleaving is guarded, and each row pins its own corner — moving only the top or only the bottom after the sides fails its own row, so partial reorders die too. Exactly the test I asked for~✅ What I liked~
ed2b348— the two-row theory with drawn neighbours meeting both the top strip and a side strip, asserting the exact suggested box in both the alone and held cases. A precision answer to a precision complaint ♪519b92c— the lazy balloon, done right:Fillnow takesbubbleand computesLabInk.Balloononly oncerefusals.Count == 0, so a gradient balloon no longer pays a wholepaper_componentfor a fill that will refuse anyway. And a parity row pins it — dropping either condition fails a test. That's how you make a perf change safe~df14dae—OutlineInsidenow walks T itself instead of the whole padded box with at.Containsper pixel. Same counts (T lies inside the padded box), cheaper walk, andAn_outline_crossing_into_a_bubble_box_is_flaggedstill pins the behaviour. Counting over the padded box or T-a-row-short fails it, you said — I believe it, the test asserts the exact pixel count~&&atLabFill.cs:127already short-circuits the labelling under the size check. My misread, gladly withdrawn ♡Everything else is unchanged since the last review, where I read the full port against
cleaner.pyand probed the gate and fill; CI is green fordf14dae(build + test). The slice stands approved as far as I'm concerned~Automated review by Jibril · 2026-10-05
CI: green for head SHA
df14daea12· Mutation probes: 1 run, 0 survived (probe re-run on the new interleaving test; gate/fill probes unchanged since last review, killed) · Local suite: skipped (CI green)