library: exclude unfired strikers (pre-struck exception) + striker schema - #141
Merged
Conversation
PR #75 renamed the SheetPanel prop to `onUpdatePieceTransform` in App.tsx but SheetPanel still destructures `onTransformChange`, so the per-piece wrapper closes over an undefined callback. Konva fires the drag events, the wrapper resolves (it's a real arrow fn), then crashes trying to call undefined → "i is not a function" repeats on every `_drag` and `_endDragAfter`. Side effects: no live preview while moving, dragged pieces only land on the next click, occasional stale selection. Rename the App-side prop back to `onTransformChange` so it matches what SheetPanel actually destructures. No SheetPanel changes needed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…r multi-color mixes
…ns like clover and avocado
…age, and fix Vanilla Cream Opal category
…classifier correctly handle Blackberry Cream
…utomatically in the scraper
…efault sorting view
…s for external hosting
…duct link and front-lit indicators, and commit registry to Git
…he library build Replaces the images[0] positional rule in scripts/build_swatch_library.py with an argmax over every gallery candidate, scored by the vendored research/delighting-035 picker (scripts/swatch_picker.py + its report-019 dependency scripts/audit_flagger.py, provenance headers in each). Addresses glass-library-integration-review.md Addendum 2 (SGE image picking structurally broken) and Blocker-1 patch #2 (below-floor products get Quarantined instead of shipping a bad image). - Candidates are scored on 320px CDN thumbnails (cached, idempotent); full images are re-downloaded only when the final pick differs from what is already on disk. - Anti-churn stability rule: a shipped image is only replaced when the picker's margin over it exceeds 0.15 (and never protected when it no longer clears the 0.45 floor). Rationale documented in apply_stability_rule(); suppressed 223 of 410 argmax disagreements in the full rebuild while allowing 187 genuine fixes through. - Failed re-fetches now revert image_url to the previously-shipped URL so a network outage cannot wedge an item into a registry/disk mismatch. - Legacy quarantine-list, -v2 preference, formula dedup, crop/calibration logic kept. - docs/library-picker-rebuild/: before/after diff report + 20-change contact sheet from the full rerun (registry/images themselves are gitignored runtime data in frontend/public/assets/, regenerated by running this script). Both maintainer SGE validation cases verified fixed in the final registry (youghiogheny-yuf5100gr -> gallery position 4, youghiogheny-y700hs -> position 3); reactive/Alchemy recovered set ships the -v2 images, unrecoverable set excluded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-white false quarantines
Decision 3 (glass-library-integration-review.md Addendum): replace the blanket
is_reactive_cloud drop rule with REACTIVE_CLOUD_CROP_OVERRIDE -- the two
000009-0030/-0050 SKUs are re-included using the adjudicated clean crop
(x:[650,1200], y:[0,1200] of the 1200x1200 -v2 image), which excludes the
reaction-demo tile corner insert entirely. Adjudication recorded in research
trunk's refetch_manifest.json (recovered entries with human_verification_caveat).
Decision 4 (036 report): WHITE_ON_WHITE_OVERRIDE restores the two products the
picker floor wrongly quarantined (Opaque White 000013-0030, silver-gray Cascade
002249-CA37) via an explicit per-SKU list instead of lowering the floor globally.
Decision 5: registry entries now carry a 'lighting' field ('front-lit'/'back-lit')
derived from the report-015/019 per-manufacturer priors, alongside the existing
front_lit bool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ership overrides Replaces the stale pre-rebuild registry (1333 entries, committed in 06ac88e) with the current picker-corrected one from the 036 rebuild (1265 entries; docs/library-picker-rebuild/report.md), plus the four ownership-cleanup entries: - bullseye-0000090030f1010 / -0000090050f1010: Reactive Cloud KEEP+crop (Decision 3) -- point at the -cropped.jpg derived from the -v2 image, crop_box [650, 0, 1200, 1200], real-world width scaled accordingly. - bullseye-0000130030f1010 / -002249ca37f1010: white-on-white false-positive restores (Decision 4). - every entry now carries the 'lighting' field (Decision 5). The 548MB catalog_images/ stay untracked; run python3 scripts/build_swatch_library.py to fetch them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-setup docs Decision 2 (review Blocker 2): with the registry committed but the 548MB catalog_images/ untracked, a clean checkout must degrade loudly, not silently: - registry fetch failure now renders an explicit setup message (run scripts/build_swatch_library.py) instead of the generic no-matches empty state - per-thumbnail onError fallback shows an 'image not fetched' note instead of a broken image when catalog images 404 - README gains a 'Glass swatch library data' section documenting the two-part data layout and the fetch/refresh workflow - docs/library-picker-rebuild/OWNERSHIP.md records ownership and the decision log for the integration-review cleanup Decision 5: the front-lit warning badge now keys off the registry's new 'lighting' field (front_lit kept as fallback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…product The Phase B dedup loop skips picker-Quarantined rows before the REACTIVE_CLOUD_CROP_OVERRIDE is consulted, so a future run in which the picker floors both of the product's (borderline-by-construction) hybrid photos would silently drop the adjudicated KEEP+crop entries. Route the reactive-cloud ids through apply_manual_overrides too, restoring status Downloaded with the -v2 source URL; Phase B/C then apply the crop as usual. Verified with a unit-level harness: quarantine->Downloaded conversion for both override lists, pass-through for other ids, and the crop fast-path returning the 550x1200 cropped files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erified border-scrubbed crops (#124) * feat: prefer Bullseye whole-sheet/grip photos over flat renders, with hardware crop Diagnosis: Bullseye galleries lead with a full-bleed solid-color macro render (_01) that scores perfectly on the picker's audit/coverage components (no background, edge-to-edge 'sheet'), while the real whole-sheet photo -- white studio ground, sometimes clamp grips on the right edge, a product label and a sharpie batch number -- is penalized as advisory product_on_white (audit 0.35) and capped on coverage by its white border. The flat render therefore beat the real sheet photo by 0.04-0.28 base score wherever both existed. Fix (swatch_picker component (f), Bullseye-only, no ML): - grip/whole-sheet detector on downloaded pixels: near-white ground fraction, dominant-sheet blob, dark clamp blobs touching the right edge - relative rank adjustment: grip +0.35 / whole-sheet +0.15 / flat chip -0.30, the penalty only when a sibling candidate is itself a whole-sheet photo - crop box (fraction coords) excludes the clamps, label column (fixed 80%%-of- sheet-width right cut) and the sharpie strip (12%% top inset); applied in the build's existing crop_box pipeline at full resolution - churn gate: a shipped Bullseye pick may only change when it is the diagnosed flat-chip -> detected whole-sheet/grip upgrade; everything else keeps the current image and is listed in the report (6 kept this run: 2 white-on-white sheets, 2 photo-vs-photo taste swaps, 2 pale striker tints whose grip photo the detector cannot yet crop safely) - --only-manufacturer scoping: re-runs one manufacturer's collection, carries every other manufacturer's registry rows through verbatim - legacy-quarantine -v2 rows are carried through when the recovery file is absent in the checkout instead of silently dropping shipped products Result: 273 Bullseye picks upgraded (242 whole-sheet, 31 grip), all 273 with hardware/label/sharpie cropped out; 0 rows removed; non-Bullseye rows byte-identical. Review board (old|new for every change): docs/library-picker-rebuild/bullseye_grip_review.jpg Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: full-resolution border scrub for Bullseye grip crops (no background survives) Lead review of the first board found a systematic artifact: most crops kept a thin near-white sliver along the bottom edge (plus occasional corner wedges). Root cause is two-fold: (1) the sheet bbox's bottom rows are a bright specular reflection band with a DARK cut-edge line below it, so an outermost-row check stops at the dark line and traps the white band inside the crop; (2) the band's depth varies 17-38px per photo and, at the picker's 320px thumb scale, JPEG downsampling blends it below the white threshold entirely -- a thumb-scale-only scrub passed 47 crops that were still dirty at full resolution. The scrub (swatch_picker._scrub_crop_box) is redesigned and now runs twice: - cheap thumb-scale pass in bullseye_features() as the churn-gate prefilter - AUTHORITATIVE full-resolution pass in the build's Phase C on the actual downloaded pixels, immediately before cropping Mechanics: scan a deep window (15% of the crop) in from each edge, cut past the deepest near-white-contaminated row/col, apply a safety inset on all four sides (err toward trimming sheet, never keeping background), then SELF-VERIFY the final crop's border bands are clean. A scrub that loses >20% of the crop area or fails verification returns failure -- the product keeps its currently-shipped pick and is listed as uncertain instead of shipping a bad crop. Result vs the branch base: 232 Bullseye picks upgraded (all flat-chip -> whole-sheet/grip, every crop re-verified at full res: zero border rows/cols above the 5% near-white QA threshold across all 232). 48 products are now held as uncertain (was 6): pale/white opalescents, white-mix streaky lines and bright iridescents whose near-white content cannot be safely rectangle-cropped -- listed in docs/library-picker-rebuild/report.md. 0 rows removed; all non-Bullseye rows byte-identical. Review board regenerated: docs/library-picker-rebuild/bullseye_grip_review.jpg Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rdering bug (#128) PR #124's crop phase only ran on picks that changed that run, so products whose already-shipped pick was itself an uncropped grip/whole-sheet photo never got the border scrub. Swept all 217 Bullseye rows shipped with cropped:false by classifying pixels (not URL/filename heuristics) through the existing grip/whole-sheet detector, applying the same authoritative full-res scrub + hard-reject discipline as #124. Found and fixed a real ordering bug in swatch_picker._scrub_crop_box: the left/right contamination check used the pre-row-trim row range, so a contamination band confined to the top/bottom (e.g. a specular reflection strip near the cut edge) got double-counted as contaminating every column, forcing a false symmetric left+right trim that blew the 20% area-loss budget on otherwise-croppable grip photos (confirmed on the four products named in the task: Butterscotch/Marigold Yellow/Plum Striker/Gold Purple Opalescent). The final self-verification band is unchanged and still catches genuine residual contamination regardless of ordering. Also wrote a bespoke black-corner scrub (new contamination class: a black mount/frame wedge in the top-left AND bottom-left corners, invisible to the white-background detector since white opalescent glass reads as its own background) for Dense White Opalescent Thin-rolled, and resolved the two other user-named cases by fetching full product galleries: Clear Transparent 3mm has no flat alternative to its shipped side-view (kept, flagged for a second source); Light Mineral Green's washed gradient look is the true appearance of the finish across all 3 candidates, and the picker's raw preference for a tighter macro crop would repeat the flat-chip anti-pattern #124 demotes, so the shipped pick was kept. Result: 8 of 217 newly cropped, 19 hard-rejected to the uncertain list (kept uncropped, mostly false-positive whole-sheet detections on pale glass), 188 correctly left alone. Bullseye: 358 cropped / 209 uncropped of 567. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…+ raw results) scripts/vlm_pick_judge.py: re-scrapes each sampled product's full live gallery (throttled ~1 req/s/host), builds a numbered contact sheet, and asks one 'claude -p' subprocess per (product, model) to forced-choice pick the straight-on flat swatch photo (strict parse, one retry). 41-product sample = 4 user-named Oceanside failures + a low/high pick_score mix across all 4 manufacturers. Judged with both sonnet and haiku. Registry read-only. scripts/vlm_judge_report.py: renders the review board + pilot report from the results (committed next). docs/library-picker-rebuild/vlm_judge_pilot_results.json: raw per-product results (heuristic index, both judges' picks, latency, cost). The data/ working dir (thumbs, contact sheets, caches) stays gitignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deliverable A: docs/library-picker-rebuild/vlm_judge_pilot_board.jpg -- 41 rows, heuristic pick | sonnet pick | haiku pick per product, named failures highlighted. Deliverable B: docs/library-picker-rebuild/vlm_judge_pilot.md -- named- failure verdicts (3 fixed outright, 1 improved-to-quarantine, 1 already fine on the current gallery), agreement stats (sonnet-haiku 73%), NONE-rates (sonnet 2%, haiku 12% incl. a verified false NONE), measured cost/latency, corpus projection ($68 sonnet / $21 haiku for 1,269 products), recommendation: one-time sonnet judge-everything backfill -- confidence gating is not viable because all named failures had HIGH heuristic scores (1.0-1.5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cro fix Sonnet forced-choice judge over the 5 flagged products (5 calls, $0.26, zero parse failures), applied through the registry per the sweep_uncropped_bullseye.py precedent: - bullseye-0002430030f1010 (Translucent White 3mm): CHANGED #4 -> #1. Shipped pick was the product-label macro (_04.jpg, heuristic score 0.9044). Winner is the flat white sheet on a BLACK ground -- added a self-verifying dark-edge trim (generalizing report.md's bespoke dark-corner scrub, as it requested), crop [46,38,1192,1162], 89% kept. Row carries pick_score=null + vlm_judge provenance marker. - bullseye-0011010000f1010 (Clear single-rolled): NONE as predicted -- single-image gallery (side view); kept, confirmed second-source case. - bullseye-0012470031f1010 (Mineral Green irid): CONFIRMED shipped _02 over the macro _01, independently validating the sweep's reasoning. - bullseye-0001130050f1010 / bullseye-0002430050f1010 (white glass): both CONFIRMED; the judge did not fall for the 000243-0050 gallery's own label macro. WARNING documented in vlm_judge_targeted.md: the current picker still argmaxes the label macro 0.907 vs 0.603 (margin 0.30 > stability 0.15), so an unguarded full rebuild will churn this row back -- needs a vlm_judge-marker guard in build_swatch_library.py (not modified here; concurrent-edit freeze). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build_swatch_library.py's rebuild had no knowledge of the vlm_judge provenance marker vlm_judge_targeted.py writes onto a registry row: the stability rule only protects a previously-shipped image when its OLD score clears PICKER_FLOOR, but a judge-picked row always has pick_score=None and typically isn't even in this run's freshly-scraped candidate_scores -- so a plain rebuild silently reverted judge overrides (e.g. Translucent White) straight back to the heuristic's confidently-wrong pick. Guard now checks the existing row for a vlm_judge marker before the picker-quarantine skip, the stability rule, and the Bullseye churn gate, and carries the row through byte-for-byte when present -- same posture as the existing carried_v2_rows pattern. Verified with a monkeypatched rebuild simulation: the Translucent White judge row survives a rebuild where the heuristic argmax tries to re-pick the label-macro image, while a non-judge-marked control row is still processed by the normal machinery.
Adds a corpus-wide, structured-signal-based striker classifier to build_swatch_library.py rather than relying on literal "Striker" name matches (which miss real cases -- Translucent White and Salmon Pink both ship pale/ unfired but neither has "Striker" in its name): - Bullseye: membership in the vendor's own `strikers` Shopify collection, reduced to base color codes (independent of thickness/format variant). This is the same signal that drives the "striker" page badge/tooltip on a Bullseye product page -- a per-product metafield not exposed on the regular /products.json feed (confirmed empirically: Translucent White/Salmon Pink's body_html says nothing about striking, but their live page embeds "key":"striker"/"value":"true"). 140 registry rows match (26 explicitly named "Striker" + 114 hidden, including both known cases). - Wissmach: a 'Striker' tag or a "color matures upon firing" family of phrases in body_html -- Wissmach puts this disclaimer directly on the product page. 6 registry rows match (3 named + 3 hidden: Gold Tone Opal, String of Pearls Opal Reactive, Oyster Pearl Opal Reactive). - Oceanside and Youghiogheny: investigated (tags, body_html keyword scan, and a live-page probe for a Bullseye-style metafield badge) with no striker signal found. Youghiogheny's "High Strike" line was specifically checked against Youghiogheny's own glossary and confirmed to be a different thing -- a hand-rolled, non-fusible mottled art-glass product line, already fully colored as shipped, not a kiln-refire color. Deliberately not flagged. `striker` (bool) and `strikes_to` (cleaned color-name string, or null) are now always present on every registry row. Verified with a monkeypatched rebuild covering all 4 manufacturers plus known striker/non-striker controls.
…rows Adds scripts/vlm_judge_striker.py: a sonnet judge pass over every registry row flagged `striker: true`, asking specifically for the PALE/unfired candidate (what actually ships) instead of the vivid/fired color a naive photo-quality heuristic would prefer. Also backfills striker/strikes_to onto the currently-committed registry (a full corpus rebuild will regenerate these fields going forward via the classifier landed in 939df9c). Audited so far: 34 of 146 confirmed striker rows (the 29 literal "Striker"- named products across Bullseye/Wissmach, plus the 5 hand-confirmed hidden cases: Translucent White x2, Salmon Pink x2, Wissmach Orange-Red Opal Striker Luminescent). 16 picks switched from a fired/vivid or outright wrong photo to the pale/unfired sheet; 11 confirmed already-correct; 6 have no valid pale candidate in their own gallery (left as-is, flagged); 1 manual cross-thickness override applied (Salmon Pink 2mm's own gallery's only non-vivid photo was a different, unrelated blue glass color entirely -- reused the 3mm sibling's genuine pale photo of the same base color instead). The remaining ~112 hidden (non-"Striker"-named) Bullseye striker rows are not yet individually audited -- follow-up work, tracked separately.
…ception)
CTO pivot 2026-07-16 (supersedes "switch the striker photo to its pale unfired
sheet"): unfired striker glass is now filtered OUT of the shipped library
entirely. WYSIWYG -- striker colors only develop once fired to ~1200F+, and the
~99% of users who work cold-process (foil/came) never reach that temp, so a
striker sheet keeps its dull unfired color forever; showing it (fired OR pale)
misleads them. Strikers are premium-priced (gold-derived pinks/rubies/purples).
- build_swatch_library.py: should_exclude_striker()/is_prestruck()/
partition_striker_exclusions() run as the last registry-assembly step (catches
scraped + all carry-through row sources). --exclude-strikers-in-place applies
just this filter to an existing registry with no network scrape (used here).
Schema fields striker/strikes_to kept -- they now DRIVE the exclusion.
- Registry: 1332 -> 1186 rows; 146 unfired strikers quarantined (Bullseye 140,
Wissmach 6). Every kept row byte-identical to before; documented quarantine
list in docs/library-picker-rebuild/striker_quarantine.{md,json}, not a
silent drop.
- Pre-struck exception keeps ZERO products: the brief's "S-suffix" rule does
NOT exist. Bullseye's coding system defines the trailing letter as a GRADE
code (F/P/B/C, no S, no pre-struck code); all strikers ship in F grade and no
row carries "Struck" wording. Detector kept as a defensive forward guard on
whole-word struck/pre-struck/pre-fired (never "striker").
- Identification 100% complete; Bullseye flags cross-checked against the live
strikers collection: 0 missed, 0 spurious. Oceanside 0, Youghiogheny 0,
Wissmach 6 -- cold-glass mainstays striker-sparse as expected.
- vlm_judge_striker.py marked SUPERSEDED (its photo switches are moot; excluded
rows do not ship). Reverted the uncommitted moot photo-switch changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploying vitrai with
|
| Latest commit: |
18da9c4
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9bdcbfce.vitrai.pages.dev |
| Branch Preview URL: | https://fix-vlm-judge-targeted.vitrai.pages.dev |
# Conflicts: # frontend/src/App.css # frontend/src/App.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
CTO policy change (2026-07-16, supersedes "show the pale unfired sheet"): filter unfired strikers OUT of the shipped library entirely.
WYSIWYG rationale: striker colors (gold-derived pinks/rubies/purples, premium-priced) only reach their named color once fired to ~1200F+. The ~99% of users who work cold-process (copper foil / lead came) never reach that temperature, so a striker sheet keeps its dull unfired color forever. Showing the fired color betrays the buyer; showing the pale unfired color advertises a premium sheet in its unrecognizable state. Either way it misleads a cold-process audience — so we exclude.
Result
Registry: 1332 → 1186 shipped rows; 146 unfired strikers quarantined.
Every kept row is byte-identical to before (verified); the 146 excluded rows are recorded with per-product reasons in
striker_quarantine.md/.json— a documented quarantine, not a silent drop.Pre-struck exception — keeps ZERO products, and the "S-suffix" rule does not exist
The one keep-rule is pre-struck (factory-fired, color-locked) sheets. The brief expected Bullseye to mark these with an "S" suffix on the product code (vs "F" fusible) and/or "Struck" wording. Verified against Bullseye's authoritative coding-system PDF and the live
strikerscollection: there is no "S" grade. The trailing letter is a grade code —FFusible/Tested-Compatible,PStandard Production,BFusible-curious,CCurious — with no pre-struck code at all. All 532 striker-collection variants (and all 567 of our Bullseye rows) ship inFgrade; no row carries "Struck"/"pre-struck"/"pre-fired" wording. So the reliable signal is whole-word "struck"/"pre-struck"/"pre-fired" in the name (never the nonexistent S-suffix, never "striker" itself). It is implemented as a defensive forward guard that currently matches nothing. Keep-list is empty.Coverage & correctness
striker/strikes_to(schema fields retained; they now drive the exclusion).Implementation
should_exclude_striker()/is_prestruck()/partition_striker_exclusions()inbuild_swatch_library.py, run as the last registry-assembly step (catches scraped + all carry-through row sources).--exclude-strikers-in-placeapplies just this filter to an existing registry with no network scrape — used to land the pivot on the current registry.vlm_judge_striker.pymarked SUPERSEDED (its photo switches are moot — excluded rows don't ship); the uncommitted moot photo-switch changes were reverted.🤖 Generated with Claude Code