Skip to content

library: exclude unfired strikers (pre-struck exception) + striker schema - #141

Merged
dompm merged 38 commits into
mainfrom
fix/vlm-judge-targeted
Jul 17, 2026
Merged

library: exclude unfired strikers (pre-struck exception) + striker schema#141
dompm merged 38 commits into
mainfrom
fix/vlm-judge-targeted

Conversation

@dompm

@dompm dompm commented Jul 17, 2026

Copy link
Copy Markdown
Owner

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.

Manufacturer Total Excluded Shipped
Bullseye 567 140 427
Oceanside 283 0 283
Wissmach 217 6 211
Youghiogheny 265 0 265
Total 1332 146 1186

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 strikers collection: there is no "S" grade. The trailing letter is a grade code — F Fusible/Tested-Compatible, P Standard Production, B Fusible-curious, C Curious — with no pre-struck code at all. All 532 striker-collection variants (and all 567 of our Bullseye rows) ship in F grade; 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

  • Identification is 100% complete — all 1332 rows carry striker/strikes_to (schema fields retained; they now drive the exclusion).
  • Bullseye flags cross-checked against the live strikers collection this run: 0 missed, 0 spurious (we stock 58 of the collection's 63 base color codes; the other 5 are frit/rod/billet formats we don't carry).
  • Cold-glass mainstays striker-sparse as the CTO expected: Oceanside 0/283, Youghiogheny 0/265, Wissmach 6/217; Bullseye (kiln-glass company) striker-dense at 140/567. Kokomo not in corpus.

Implementation

should_exclude_striker() / is_prestruck() / partition_striker_exclusions() in build_swatch_library.py, 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 to land the pivot on the current registry. vlm_judge_striker.py marked SUPERSEDED (its photo switches are moot — excluded rows don't ship); the uncommitted moot photo-switch changes were reverted.

🤖 Generated with Claude Code

dompm and others added 30 commits May 24, 2026 09:55
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>
…classifier correctly handle Blackberry Cream
…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>
dompm and others added 7 commits July 13, 2026 15:21
…+ 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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploying vitrai with  Cloudflare Pages  Cloudflare Pages

Latest commit: 18da9c4
Status: ✅  Deploy successful!
Preview URL: https://9bdcbfce.vitrai.pages.dev
Branch Preview URL: https://fix-vlm-judge-targeted.vitrai.pages.dev

View logs

# Conflicts:
#	frontend/src/App.css
#	frontend/src/App.tsx
@dompm
dompm merged commit 2025f64 into main Jul 17, 2026
2 checks passed
@dompm
dompm deleted the fix/vlm-judge-targeted branch July 17, 2026 03:10
dompm added a commit that referenced this pull request Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant