Repository navigation
feat: card scanner Phase 1 — live capture and re-measure (007) - #8
Merged
Merged
Conversation
plainprogrammer
marked this pull request as ready for review
October 2, 2026 14:35
plainprogrammer
added a commit
that referenced
this pull request
Oct 2, 2026
Memories: card scanner status after Phase 1 (PR #8, corpus cards returned, tuning cards biased, go/no-go pending); check that physical corpus items will be available before specifying a re-measure; background dev servers stop at the 2-hour task limit. Lessons: tune OCR on stored strips (desktop replays equal the device's text); framing dominates scanner accuracy, and a collector-line match can be wrong. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 tasks done
Card scanner Phase 1: live capture and re-measure. Spec v1.1.1 (approved, reviewed twice on Fable); plan revised after its Fable review. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each ADR moves from Proposed to Accepted and records how it changed from its Proposed text. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rd set codes Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…name fallbacks catalog_names holds every name a catalog identity is known by (identity names from the core, face names from MTG via the optional .alternate_names source contract method), with an external-content FTS5 trigram index over the normalised name. Catalog::NameIndex rebuilds it in full and searches it: query cleaning (AC-3.4), OR-ed trigrams shortlisted by bm25 and re-ranked by Jaro-Winkler, one candidate per identity, plus the within-one-edit and exact-raw fallbacks (AC-3.5). Ruling: the retired-card example asserts the retired card is absent rather than that the result is empty — OR-ed trigrams return "Fire // Ice" (score 0.44) on the shared trigram "ire", and the spec sets no score floor — cost if wrong: a floor is wanted after all and gets added during tuning (Phase 10), with this assertion tightened then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…when it's empty Catalog::Refresh rebuilds the type's name index in full before finishing an applied run, and a scheduled run skipped for an already-applied version rebuilds it when it is empty, noting the count in the run's message (spec 007 AC-3.9, FR-5). db:prepare never backfills it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d candidates Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he image build bin/fetch-ocr-engine downloads the three pinned npm tarballs (tesseract.js, tesseract.js-core and eng 4.0.0_best_int), checks each tarball's and each file's SHA-256, and unpacks the six LSTM-only engine files into the ignored vendor/ocr/v7.0.0/. bin/setup (so also bin/ci) and the Dockerfile build stage run it (spec 007 AC-7.3, ADR 0001). Ruling: chore instead of build — build isn't an allowed type in docs/git-convention.md — none Ruling: spec's `contents` is a helper method, not a let — RuboCop's RSpec/MultipleMemoizedHelpers caps a group at 5 lets — none Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OcrAssetsController serves only the pinned engine files at /ocr/v7.0.0/*path, with Cache-Control max-age=31536000, public, immutable, a fixed ETag and Last-Modified, and 304 for If-None-Match or If-Modified-Since. Anything else is 404 (spec 007 AC-2.2, ADR 0001). Ruling: Brakeman's SendFile warning is ignored in config/brakeman.ignore — the path comes from the FILES allowlist via path_for, never from the request — a future edit that bypasses path_for would be hidden by the fingerprint only if the send_file call stays identical Ruling: ::Collector::OcrEngine instead of Collector::OcrEngine — inside a controller a bare Collector resolves to ActionController::MimeResponds::Collector — none Ruling: skip_forgery_protection on the controller — Rails' cross-origin JavaScript check answered every .js GET with 422; the action is GET-only and touches no session state — none Ruling: expires_in 365.days instead of 1.year — 1.year is 365.2425 days (max-age=31556952), the plan and spec ask for 31536000 — none Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oint GET /scanner renders the camera page (disabled, busy shutter; Phase 6 brings it to life) with a scanner-only Content-Security-Policy and a per-request nonce. POST /scanner/readings turns the recognised text into the result HTML as a Turbo Stream update of #scanner_result. Other pages send no policy and no nonce; nothing links to the scanner yet. Ruling: ScannersHelper#ocr_engine_path uses ::Collector::OcrEngine — a bare Collector can resolve to ActionController::MimeResponds::Collector in view contexts — none if wrong, the leading :: is merely redundant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Camera and card-reader Stimulus controllers over scanner/geometry and
scanner/recognition; system specs draw a synthetic card into a substituted
getUserMedia stream (ADR 0002) and run real Tesseract on it. Scanner spec
green 10 runs in a row.
Ruling: `.c-scanner [hidden] { display:none; }` replaces the per-part
[hidden] rules — `.c-btn`'s display:inline-flex overrode the hidden
attribute, so the Torch, retry and Sign in buttons showed while hidden
(caught by the torch and HTTPS specs) — cost if wrong: none beyond the
rule; hidden is meant to hide.
Ruling: the 360px spec visits the collection page before opening the
narrow frame — the scanner page's own frame-src 'none' (AC-2.4) blocks it
from hosting the iframe, so the frame stayed about:blank — cost if wrong:
none; the framed page is still the scanner with its policy.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…kips and desktop replay
Measurement mode (spec 007 Story 5, FR-6) exists only when
config.x.scanner_measurement is set (development by default). The
manifest row is chosen before the shutter; each capture's text and both
strip PNGs go to <dir>/<file>/capture-NNN*, outside the repository. The
first capture of a row is measured, later ones are retakes; rows can be
skipped. A local-only, sign-in-free replay page re-reads stored strips
through scanner/recognition, driven by script/scanner/replay.rb.
Ruling: #measurement_panel [hidden] { display:none; } in additions.css,
documented in components/Scanner.md — the panel sits outside .c-scanner,
so c-btn's inline-flex would otherwise show its hidden retry button —
cost if wrong: one id-scoped rule to fold into a c-* class later.
Ruling: Brakeman SendFile warning on StripsController#show ignored in
config/brakeman.ignore — the path comes from a manifest row matching
FILE_NAME plus STRIPS.include?, never raw request input, and the route is
local-only in development — cost if wrong: a path traversal on a dev
server, bounded by those allowlists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e sizes in development Ports spec 005's scoring into Collector::ScannerFindings so Phase 0, Phase 0's text through Phase 1's matcher, and the live run are rated the same way, with a Report for the findings Markdown and text-only fixtures, the scanner:findings and scanner:ground_truth tasks, and a development-only Collector::RequestLog middleware (COLLECTOR_REQUEST_LOG=1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Draft findings for spec 007: Phase 0's committed OCR text read again through MTG::Reading. The Phase 0 column reproduces research.md §3 (top 3 name only 26/50, exact printing 7/45); with Phase 1's matcher, exact printing is 15/45 and top 3 final ranking 33/50. The live run follows in Phase 11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ement mode COLLECTOR_HTTPS=true now sets assume_ssl in development too, so a phone reaching the dev server through an HTTPS tunnel (hosts allowed via Rails' RAILS_DEVELOPMENT_HOSTS) passes CSRF origin checks. The README covers the dev tunnel, measurement mode, and HTTPS for self-hosters on Compose and Kamal; CLAUDE.md records the scanner's facts. Ruling: Kamal wording says "uncomment" proxy/ssl/host and COLLECTOR_HTTPS rather than "set" — config/deploy.yml already ships them as commented lines — cost if wrong: a self-hoster adds duplicate keys instead of uncommenting. Ruling: the Compose bullet points to "HTTPS under Docker Compose" instead of "HTTPS above" — the HTTPS note is a bullet inside the Compose section, not its own heading — cost if wrong: none beyond wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rebase onto main (spec 006) put Scanner at the end of spec 004's list; give it its own line like 006's additions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Cloudflare tunnel returned 502 with no request reaching Rails; a self-signed certificate for the LAN IP served by Puma's ssl:// bind works, and the iPhone (as a Continuity Camera in desktop Brave) scanned two cards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collector::DevCertificate creates (or reuses) a self-signed CA certificate for the machine's LAN addresses outside the repository, and serves only the certificate over plain HTTP so the phone can install and trust it. Puma serves the pair directly with an ssl:// bind; no app change. Ruling: self-signed certificate + Puma ssl:// bind instead of a tunnel — the maintainer's Cloudflare tunnel returned 502 with no request reaching Rails; this route was verified on 2026-10-01 — the iPhone must trust the certificate once Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's phone section now leads with bin/dev-certificate, the bin/dev ssl:// bind, bin/dev-certificate --serve and the iPhone trust steps; a tunnel stays as the alternative. CLAUDE.md points to the same route, and the plan records the amendment for Phases 10-11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tch strips (tuning round 1)
Tuning round 1 (10 non-corpus cards, iPhone Brave, runs/tuning-1): name read
0/10, top 3 final ranking 6/10, exact printing 5/7, collector lookups one 5 /
none 2. Name strips sat too high and clipped the name on every card; grey
collector lines on dark borders read empty.
Round 2 settings: name strip { x: 0.05, y: 0.055, w: 0.75, h: 0.11 };
every strip drawn at STRIP_SCALE (2x), then grayscale with a per-strip
contrast stretch. Recognition settings unchanged (name psm 7, collector psm 6).
Ruling: feat instead of tune — tune isn't an allowed type — none
Ruling: name strip y 0.03→0.055, h 0.085→0.11; strips drawn at 2× with grayscale contrast stretch — round 1's name strips clipped the name on every card and low-contrast collector lines read empty — wider name strip may take in more art on some frames, which round 2 will show
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…v server SCANNER_URL overrides the replay's base URL and Firefox accepts the self-signed certificate, so script/scanner/replay.rb runs against the HTTPS dev server the phone uses. README notes the override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r strip (tuning round 2) Round 2 (10 non-corpus tuning cards, iPhone Brave, runs/tuning-2) with round 1's settings: name read 0/10, top 3 name-only 4/10, top 1 and top 3 final ranking 8/10, exact printing 6/7 (lookups one 6, none 1). The names were inside the strip, but page segmentation 7 garbled them amid frame lines and borders. T009 (Vrondiss, AFC) was held slightly larger than the guide and its "AFC • EN" line was cut off at the collector strip's bottom. Round 2's stored strips replayed on the desktop (script/scanner/replay.rb; desktop text matched the iPhone's exactly), varying only the name strip's page segmentation: | name psm | top 3 name-only | top 1 final | top 3 final | exact printing | |-------------|-----------------|-------------|-------------|----------------| | 7 (round 1) | 4/10 | 8/10 | 8/10 | 6/7 | | 6 | 10/10 | 10/10 | 10/10 | 6/7 | | 11 | 4/10 | 7/10 | 7/10 | 6/7 | | 13 | 1/10 | 6/10 | 6/10 | 6/7 | The collector strip now runs to the card's bottom edge. The synthetic test card sizes its text from the card instead of the strips. Ruling: name strip page segmentation 7→6 — on round 2's stored strips replayed on the desktop, psm 6 put every card's name first (10/10 vs 4/10) — chosen on the same 10 tuning cards it was measured on, so round 3 captures check it on fresh photos Ruling: collector strip y 0.905→0.89, h 0.085→0.11 — T009's set line was cut off when the card was held slightly larger than the guide — untested until round 3 Ruling: feat instead of tune — tune isn't an allowed type — none Ruling: the synthetic test card now sizes and places its text from the card rather than the strips — sizing from the strip made the test text grow with the strip and OCR misread it (R0O123) — none for real cards; the test is closer to them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing round 3) Round 3 (12 fresh tuning captures, settings of 7d4f09d): top 3 name-only 11/12, top 1 final 10/12, top 3 final 11/12, exact printing 6/9 (lookups one 7, none 2). The collector misses were small light-on-dark text in the card's black border: a faint foil line read "J"; "FRA" read "ERA"; "178/184 C" lost "178/" and parsed as a different real printing (AER 184), which then ranked first ahead of the correct name candidate. After the contrast stretch, cropStrips now inverts every pixel row of the collector strip whose mean luminance is below 128, so the light-on-dark collector line becomes dark-on-light while the bright text-box rows stay as they are. Which strips get it is the INVERT_DARK_ROWS tuning setting. Measurement mode stores the strips exactly as OCR read them. Replay of the stored strips of rounds 2 and 3 (22 captures; desktop replay text equals the device's exactly), variants applied before OCR: | variant (collector strip unless stated) | round 2 printing | round 3 printing | round 3 top 1 final | |---|---|---|---| | none (current) | 6/7 | 6/9 | 10/12 | | invert dark rows | 6/7 | 8/9 | 11/12 | | invert dark rows + Otsu binarize | 4/7 | 5/9 | 10/12 | | invert dark rows, psm 4 | 6/7 | 7/9 | 11/12 | | invert dark rows, psm 11 | 6/7 | 8/9 | 11/12 | | invert dark rows on name strip too | name top 3 fell to 9/10 and 9/12 | 8/9 | 11/12 | Ruling: invert dark rows of the collector strip only — on the 22 stored captures of rounds 2 and 3 it lifted round 3's exact printing 6/9→8/9 and changed nothing in round 2; binarizing and other page-segmentation modes were no better, and inverting the name strip hurt names — chosen on the same tuning cards it was measured on, so the round 4 captures check it on fresh photos Ruling: no set-code correction — one-substitution neighbours are ambiguous (ERA → EMA, FRA) and would invent matches for about a quarter of random misreads — T012-style misreads stay unmatched by the collector line and fall back to the name Ruling: feat instead of tune — tune isn't an allowed type — none Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d run Frozen after 4 tuning rounds on 12 English cards outside the 50-card corpus (3 pre-M15, 7 M15–ONE, 2 MOM+; 3 foils), captured in Brave on the iPhone (spec 007 AC-6.1). No setting changes until the findings are written. Final settings: - GUIDE: height 0.8 of the 3:4 stage, max width 0.9 (unchanged) - STRIPS.name: x 0.05, y 0.055, w 0.75, h 0.11; page segmentation 6 - STRIPS.collector: x 0.03, y 0.89, w 0.55, h 0.11; page segmentation 6 - STRIP_SCALE 2, grayscale min–max contrast stretch on both strips - INVERT_DARK_ROWS: collector only Rounds (fresh captures; top 3 name-only / final top 1 / final top 3 / exact printing): - 1 (initial settings, 10 cards): 1/10, 6/10, 6/10, 5/7 - 2 (ca27148, 10 cards): 4/10, 8/10, 8/10, 6/7 - 3 (7d4f09d, 12 cards): 11/12, 10/12, 11/12, 6/9 - 4 (ec71cd9, 12 cards): 10/12, 9/12, 10/12, 6/9 Between rounds, desktop replays of stored strips (identical to the device's text) chose page segmentation 6 and collector dark-row inversion. Ruling: freeze after round 4 — rounds 3 and 4 differ only by photo-to-photo OCR variance, and more tuning on 12 cards would chase it — the measured run may show weaknesses these 12 cards didn't. Ruling: a "leading words" name query was tried on every run and dropped — no gain in tuning, 3 fewer first places on Phase 0's text — none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…007 v2.0.0) The 50 corpus cards were borrowed and returned, so they can't be re-captured live. Maintainer ruling (2026-10-01): replay the 50 Phase 0 photos through the shipped photo-picker path as the measured run; report the four tuning rounds' live iPhone captures as the only live-alignment evidence, labelled as biased (new AC-6.8); take on-device timings from the tuning rounds and download sizes from an iPhone load test. MAJOR: rewrites Goals and AC-6.2–AC-6.6. Plan Phase 11 rewritten (11a harness, 11b runs and findings). No FR changes; no built code is invalidated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rement mode Scanner::MeasurementRun#photo_path finds a row's photo beside the manifest. GET /scanner/measurement/photos/:id serves it to this machine only (local, development-only, unauthenticated like the strips). script/scanner/photo_run.rb signs in, opens /scanner/measurement in headless Firefox and hands each pending row's photo to the real picker, waiting for the panel to report it stored. Ruling: the photo's content type comes from Marcel (magic bytes, then name) — the corpus is JPEG but a tuning folder could hold HEIC or PNG — cost if wrong: a wrong type header on a local-only route; the picker reads the blob anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collector::ScannerFindings::Report takes label: (the third column and the
misses heading; default "Phase 1 live") and prefix: (the fixture file names;
default "phase1"). scanner:findings passes RUN_LABEL and FIXTURES_PREFIX.
Ruling: the fixtures' "run" field is now the label ("Phase 1 live" by default,
was "live") — the plan says run is set from the label, and no phase1 fixture is
committed yet — cost if wrong: a reader of format_version 2 expecting "live".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text only, format_version 2, keyed by manifest file (spec 007 AC-6.6, v2.0.0): the 50 Phase 0 photos replayed through the photo-picker path at the frozen settings (c68ffbd), and tuning round 4's 12 live iPhone captures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Photo replay of the 50 Phase 0 photos through the photo-picker path, the four tuning rounds' live iPhone captures (labelled as biased: they also chose the settings), Phase 0's text re-scored with Phase 1's matcher, the iPhone load test, device checks, replays, coverage, fixtures, findings for the next spec and the maintainer's options (spec 007 v2.0.0, AC-5.4–AC-5.6, AC-6.2–AC-6.8). No pass threshold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the Fable Mode B review (SPEC-ALIGNED, 3 should-fix): - research.md: report the camera resolution (FR-2) — requested 1920×1080 ideal; a 1080×1920 portrait frame inferred from round 1's strip sizes, not logged; say "upper median" for even n; note lookup timings vary per run; state that no lookup was ambiguous (AC-6.2). - data-model.md: the fixture names are phase1_photos_* and phase1_tuning4_*. - plan.md: the Goal paragraph reflects spec v2.0.0; the load-test snippet's first /scanner row can be a signed-out 302. - ADR 0001: Consequences now match its Changes section (14,828,864 bytes, fetched, not committed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Memories: card scanner status after Phase 1 (PR #8, corpus cards returned, tuning cards biased, go/no-go pending); check that physical corpus items will be available before specifying a re-measure; background dev servers stop at the 2-hour task limit. Lessons: tune OCR on stored strips (desktop replays equal the device's text); framing dominates scanner accuracy, and a collector-line match can be wrong. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The maintainer ruled on research.md §12: re-measure live first, on 50 cards they own, none among Phase 0's 50 or the 12 tuning cards, at the frozen settings (c68ffbd), with one unguided photo per card replayed through the photo picker as a baseline on the same cards. New AC-6.9; AC-6.1 and AC-6.4–AC-6.8 amended to cover it. No pass threshold, no FR changes. The plan gains Phase 11c for the run, scoring and findings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two of the maintainer's rows repeated Phase 0 printings (hob 225, wot 49) and were dropped so AC-6.9's "none among Phase 0's cards" holds. List reprints and a retro-frame card print no set code, so the manifest gives them the pre-M15 era. Section references follow research.md's renumbering. Ruling: drop the two overlapping rows — maintainer's choice, keeps the Phase 0 comparison clean — costs one card's worth of sample size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text only, format_version 2, keyed by the new corpus's manifest file: 49 live iPhone captures and the same 49 cards' photos through the photo picker (spec 007 AC-6.6, AC-6.9). No images. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 cards outside tuning and Phase 0, captured live on the iPhone at the frozen settings (c68ffbd): right card first for 42/49, top 3 for 45/49, exact printing for 34/44 set-line cards; median recognition 168 ms. The same cards' photos, framed tighter than the guide, reached 2/49. Three misread collector numbers matched real printings of other cards and outranked the right name. New section 5; later sections renumbered; summary, findings and options updated (AC-6.4–AC-6.9). Ruling: IMG_6763's manifest row was corrected from mul 71 to mat 71 after capture — its strip and photo show Leyline Immersion — costs nothing if wrong beyond one row's ground truth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the Fable Mode B review (SPEC-ALIGNED): research.md names spec v2.1.1, says the catalog-name read matches the front-face table, compares both photo runs on the top 3, marks the framing estimate as estimated, and reports the photo run's lookup time. The spec's Problem Statement, Goals and superseded decision, and the plan's Phase 11 note and self-review, match the 49-card corpus and Phase 11c. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Confirm flow on live capture next; a strong name match outranks a disagreeing collector-line match; the photo picker stays with framing advice; card detection later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
plainprogrammer
force-pushed
the
007-card-scanner-live-capture
branch
from
October 2, 2026 21:06
1447cb4 to
366afec
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Card scanner Phase 1: live capture and re-measure (007)
Spec:
spec.md(v2.1.1) · Plan:plan.md· Findings:research.md· ADRs 0001–0003 are now Accepted.Spec v2.0.0: the 50 Phase 0 cards were borrowed and returned, so they couldn't be re-captured live. The measured run replays their Phase 0 photos through the shipped photo-picker path instead. The four tuning rounds' live iPhone captures are labelled as biased (AC-6.8).
Spec v2.1.0/v2.1.1: after the maintainer's ruling, live accuracy was re-measured on a new corpus of 49 cards the maintainer owns, none used for tuning or in Phase 0's corpus, at the frozen settings, with an unguided photo of each card as a photo-path baseline (AC-6.9, research.md §5).
What's in it
/scannerfor signed-in users; nothing links to it yet.bin/fetch-ocr-engine, run bybin/setupand the Dockerfile, downloads Tesseract.js 7.0.0, its 3 LSTM core builds and theengdata (14.8 MB), checking every file against a pinned SHA-256.OcrAssetsControllerserves the files from/ocr/v7.0.0/withimmutablecaching, and answers conditional requests with 304.'wasm-unsafe-eval'andblob:workers. Every other page still sends no policy.MTG::CollectorLinehandles Phase 0's failures: a glued rarity letter, a lost slash, a missing rarity letter, and "one" in rules text being taken for a set code.catalog_nameswith FTS5 trigrams), rebuilt byCatalog::Refresh.MTG::Readingranks candidates, with a collector-line match first./scanner/measurement: captures, retakes, skips, a desktop replay, and a replay of corpus photos through the photo picker.bin/rails scanner:findingsscores runs with spec 005's definitions and reproduces Phase 0's numbers exactly.bin/dev-certificatemakes a self-signed certificate and prints the Pumassl://bind command. The README covers the phone and self-hosting routes.Results
Verification
bin/cipasses: RuboCop, Brakeman (3 justified ignores, allsend_filebehind an allowlist), bundler-audit, the importmap audit, and RSpec with 601 examples and 0 failures.5d22545and871331a. A second Mode B review of the re-measure (AC-6.1, AC-6.4–AC-6.9) also returned SPEC-ALIGNED; its fixes are in887898b.Ruling:line in its commit body.Notes for review
20260930170000_create_catalog_namesonly creates tables. The index fills on the next refresh: an applied run, or a scheduled run skipped while the index is empty.~/card-scanner-corpus/.🤖 Generated with Claude Code