Record Neon migration applied, scan_attempts snapshot, and backfill gate. Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
8.1 KiB
Markdown
208 lines
8.1 KiB
Markdown
---
|
||
name: scanner-identify-upgrade
|
||
classification: feature
|
||
success_metric: |
|
||
Of legitimate card scans (excluding not_a_card), ≥50% auto-match a
|
||
catalog printing with no picker; Layer-1 escalate rate falls from 76.7%
|
||
to ≤40%; median /api/scan/identify latency stays near today's 1.7s p50
|
||
or improves. Measured on scan_attempts after Phase 1 ships.
|
||
skip: []
|
||
status: open
|
||
created: 2026-08-14
|
||
depends_on:
|
||
- add-real-ocr-layer
|
||
- server-side-scan-pipeline
|
||
model_policy:
|
||
default_session: auto
|
||
roles:
|
||
role-conductor: composer-2.5-fast
|
||
role-architect: composer-2.5
|
||
role-ia-architect: composer-2.5-fast
|
||
role-ux-reviewer: composer-2.5-fast
|
||
role-ui-designer: composer-2.5-fast
|
||
role-implementer: composer-2.5-fast
|
||
role-reviewer: cursor-grok-4.5-high
|
||
role-security-auditor: gpt-5.6-terra-medium
|
||
role-design-system-auditor: cursor-grok-4.5-high
|
||
role-a11y-auditor: cursor-grok-4.5-high
|
||
role-doc-writer: auto
|
||
escalate_to: claude-sonnet-5-thinking-medium
|
||
escalate_to_premium: claude-4.6-opus-high-thinking
|
||
never_premium:
|
||
- role-reviewer
|
||
- role-security-auditor
|
||
- role-design-system-auditor
|
||
- role-a11y-auditor
|
||
- role-ui-designer
|
||
- role-doc-writer
|
||
---
|
||
|
||
# Scanner identify upgrade — epic
|
||
|
||
Umbrella for making the camera scanner **faster and more accurate**
|
||
without replacing the two-layer shape (cheap local path, then
|
||
server-owned Gemini). Planning-only in this file. Each numbered
|
||
sub-convoy is its own gated PR stream.
|
||
|
||
Worktree: `tcg-vault-worktrees/scanner-identify-upgrade` on
|
||
`convoy/scanner-identify-upgrade` (branched from `main` @ `c6c1364`).
|
||
Do **not** land these docs on `feat/scanner-mobile-checkout` — that
|
||
convoy owns scanner chrome/cart and explicitly leaves identify out of
|
||
scope.
|
||
|
||
## Why
|
||
|
||
`add-real-ocr-layer` (PR #38) targeted ≥70% of scans resolving at
|
||
Layer-1 (Tesseract + `pg_trgm`) with zero Gemini calls. Live
|
||
`scan_attempts` from the local Neon (2026-05-27 → 2026-08-12, n=224,
|
||
1 user) shows that target was missed by a wide margin:
|
||
|
||
| Signal | Value | Target / note |
|
||
| --- | --- | --- |
|
||
| L1 auto-match (`result_kind=matched`) | **5.8% of L1** / 2.7% of all | ≥70% L1 resolve |
|
||
| L1 escalate | **76.7% of L1** (79/103) | Should be the minority path |
|
||
| L1 disambiguation | 17.5% of L1 (18/103) | Name-only matcher cannot pick printings |
|
||
| End-to-end auto-match | **10.3%** (23/224) | User still picks or retries most cards |
|
||
| L2 `not_a_card` | **44.6% of L2** (54/121) | Detector fires on non-cards |
|
||
| L2 p50 / p90 latency | **1655 / 2349 ms** | Plus 3.5s hold-still *before* identify |
|
||
| L1 p50 latency | **158 ms** | Fast, but almost never uniquely matches |
|
||
| `card_submissions` | 27 pending, 0 reviewed | Catalog-gap queue is unread |
|
||
|
||
The six L1 "matches" include noisy Tesseract strips (`J ——`, `Rock
|
||
Jockey ©`) at confidence 36–68. L1 is a cheap filter that rarely
|
||
identifies a printing. Meanwhile every card waits
|
||
`DETECTION_START_DELAY_MS` (1s) + `MIN_FIRST_SEEN_MS_FOR_VERIFY` (2.5s)
|
||
before OCR starts, and L2 is capped at 5 Gemini calls / user / minute.
|
||
|
||
Users feel this as: hold still forever, then pick from a list, or get
|
||
"not a card" / "saved for review."
|
||
|
||
## Scope
|
||
|
||
### In scope (this epic)
|
||
|
||
1. **`tighten-scan-identify-hot-path`** — Phase 1. Cut the hold-still
|
||
gate, OCR collector number on L1, structured Gemini JSON, optional
|
||
`SCAN_VISION_MODEL` bump, stop the automatic second Gemini refine.
|
||
2. **`improve-scan-card-detection`** — Phase 2. Replace the 320×240
|
||
Sobel brute-force detector with a real card crop + perspective warp.
|
||
This was queued in `add-real-ocr-layer` and never opened.
|
||
3. **`scan-visual-catalog-search`** — Phase 3. Embed catalog
|
||
`image_url`s; nearest-neighbor the warped crop. Gemini becomes
|
||
fallback. This is the accuracy leap.
|
||
|
||
### Out of scope (this epic)
|
||
|
||
- Scanner chrome, cart, checkout (`scanner-mobile-checkout` /
|
||
`scanner-rebuild`). Identify libs only.
|
||
- Training a custom card CNN or standing up a Python/CUDA OCR service.
|
||
- Swapping Tesseract for EasyOCR / PaddleOCR as the *primary* identifier.
|
||
- Raising the L2 rate limit until Phase 1 hit-rate is re-measured.
|
||
- Admin review of the 27 pending `card_submissions` (ops, not this epic).
|
||
|
||
## Baseline (do not re-query to "start" Phase 1)
|
||
|
||
Pulled 2026-08-14 from local `.env.local` → Neon `scan_attempts`.
|
||
|
||
```
|
||
layer | result_kind | n | pct
|
||
1 | escalate | 79 | 35.3
|
||
1 | disambiguation | 18 | 8.0
|
||
1 | matched | 6 | 2.7
|
||
2 | not_a_card | 54 | 24.1
|
||
2 | submitted | 26 | 11.6
|
||
2 | disambiguation | 22 | 9.8
|
||
2 | matched | 17 | 7.6
|
||
2 | needs_input | 2 | 0.9
|
||
```
|
||
|
||
L1 escalate text-length buckets (all had ≥3 chars — Tesseract is
|
||
emitting text that `pg_trgm` cannot match): 3–7 chars n=28; 8–19 n=30;
|
||
20+ n=21.
|
||
|
||
Re-measure with the same grouping after each sub-convoy ships.
|
||
|
||
## Dependency graph
|
||
|
||
```
|
||
[baseline pulled 2026-08-14]
|
||
│
|
||
▼
|
||
┌───────────────────────────────────┐
|
||
│ 1. tighten-scan-identify-hot-path │
|
||
│ gates, collector #, schema JSON│
|
||
└───────────────┬───────────────────┘
|
||
│
|
||
▼
|
||
┌───────────────────────────────────┐
|
||
│ 2. improve-scan-card-detection │
|
||
│ detect + homography crop │
|
||
└───────────────┬───────────────────┘
|
||
│
|
||
▼
|
||
┌───────────────────────────────────┐
|
||
│ 3. scan-visual-catalog-search │
|
||
│ embeddings + pgvector kNN │
|
||
└───────────────────────────────────┘
|
||
```
|
||
|
||
**Strict-blockers:** #1 before #2 only if #2 would retune the same
|
||
stability constants — otherwise #1 (identify files) and #2 (detection
|
||
files) are file-disjoint and may run in parallel after Architect
|
||
confirms. #3 needs a stable crop (#2) to be worth the embedding job;
|
||
do not start #3 until #2 has a warped JPEG.
|
||
|
||
**Sibling:** `scanner-mobile-checkout` (other worktree / branch) must
|
||
not edit `lib/ocr-worker.js`, `lib/scan-vision.js`,
|
||
`lib/card-text-match.js`, `lib/scanner-card-identify.js`,
|
||
`lib/scanner-card-detection.js`, or `/api/scan/identify`.
|
||
|
||
## Roles invoked
|
||
|
||
Umbrella is planning-only. Each sub-convoy lists its own roles.
|
||
Default for #1: Architect → Implementer → audit fan-out (reviewer +
|
||
security-auditor). UX reviewer on #1 and #2. Skip IA and UI Designer
|
||
on all three (no new routes or visual language).
|
||
|
||
## Todos
|
||
|
||
- [x] Pull `scan_attempts` baseline (2026-08-14)
|
||
- [x] Open worktree `scanner-identify-upgrade` from `main`
|
||
- [x] Seed sub-convoys #1–#3
|
||
- [x] Architect: pick up `tighten-scan-identify-hot-path` first
|
||
- [ ] Re-measure `scan_attempts` after all phases ship (post-backfill)
|
||
- [x] Architect: `improve-scan-card-detection`
|
||
- [x] Architect: `scan-visual-catalog-search` after warped crops exist
|
||
- [ ] Operator: merge #162 CI fix, `npm run migrate up`, `npm run backfill-embeddings`
|
||
|
||
## Worktree
|
||
|
||
| Checkout | Branch | Purpose |
|
||
| --- | --- | --- |
|
||
| `tcg-vault-worktrees/scanner-identify-upgrade` | `convoy/scanner-identify-upgrade` | These convoy docs |
|
||
| (later) Agents Window worktree per brief | `brief/scanner-identify-upgrade/<n>-<title>` | Implementer slices |
|
||
|
||
`scripts/wt.sh` is a deprecation stub. Create implementer worktrees from
|
||
the Agents Window after Architect writes `slice_dependencies:`.
|
||
|
||
## Multitask dispatch
|
||
|
||
No implementer fan-out from this umbrella. After #1 Architect marks
|
||
parallel-safe briefs (`depends_on: []` + disjoint `files:`):
|
||
|
||
```
|
||
/multitask role-implementer briefs <ids>
|
||
```
|
||
|
||
After each PR draft:
|
||
|
||
```
|
||
/multitask role-reviewer + role-security-auditor
|
||
```
|
||
|
||
Add design-system + a11y auditors only if the PR touches
|
||
`components/` or `pages/scanner.js` (not expected in #1).
|
||
|
||
Group ids: `audit-tighten-scan-identify-hot-path-<pr>`,
|
||
`audit-improve-scan-card-detection-<pr>`,
|
||
`audit-scan-visual-catalog-search-<pr>`.
|