phase-3: ingestion, from a QR in the camera to a card in the bandeja
The whole pipeline: storage behind one driver interface (local disk and S3), a portable job queue with a poller, QR and CDC parsing, OCR through the Anthropic API, dedupe, manual entry, and the bandeja that turns all of it into one decision per card. Scanning tries the trustworthy door first: a QR is parsed and prefilled from its CDC; a photo without one is queued for OCR when a key is configured and otherwise opens the manual form against the stored file. A QR that will not parse records an ingest error and falls through rather than losing the photo. Job claiming is the only dialect divergence, as SPEC allows: FOR UPDATE SKIP LOCKED on Postgres, a conditional UPDATE against SQLite's single writer. Retry backoff follows SPEC exactly and a job abandoned by a killed process returns to the queue once its lock goes stale, which is the phase 3 acceptance case. OCR uses structured outputs rather than parsing prose, so the model cannot return anything but the RULES.md schema, and every field is nullable because unreadable is a real answer. Web: scan with live QR decoding (BarcodeDetector, ZXing fallback, wasm served from our own origin), manual entry with the IVA split worked out from the total, the bandeja with swipe, buttons and keyboard all doing the same thing, and the documents list and detail with an editable classification. The seed now carries Maria's 34 purchases and 8 sales and Carlos's 6, all classified through the real rules, plus the two open ingest errors. Two defects found and fixed with tests: seeded documents could be dated in the future, which would corrupt any projection computed from them, and the category buttons announced their keyboard shortcut as part of their name. 255 vitest tests, 37 Playwright tests, rules coverage still 100%, typecheck and lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0d7651b17c
commit
b074456b70
@@ -17,6 +17,9 @@ algorithms. Both are implemented in phase 1 and the seed can now be completed.
|
||||
**Resolved.**
|
||||
|
||||
### `boneyard` and `canvas-ui` are not the packages the prompt means
|
||||
_Still open. The `<Skeleton name>` component is in place and used on every data screen, so
|
||||
swapping in the real library is one file._
|
||||
|
||||
Both names resolve on npm to unrelated projects: `boneyard@0.1.4` is a 2015 Backbone
|
||||
"architectural toolkit", `canvas-ui@0.2.3` is a Mesosphere Bootstrap theme. Neither does
|
||||
skeleton loading or canvas effects. Neither is needed before phase 7.
|
||||
@@ -273,3 +276,94 @@ bounds in both states, because this is invisible to every other kind of test.
|
||||
`/legal/privacidad` and `/legal/terminos` render "Documento en preparacion" and say a
|
||||
document is being drafted, per COPY.md section 13. No legal text was generated.
|
||||
**TODO: human written before launch.**
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Phase 3
|
||||
|
||||
### The pipeline has three doors, and the trustworthy one wins
|
||||
`ingestScan` tries the QR first, then queued OCR, then the manual form. A QR that will not
|
||||
parse is recorded as an ingest error and falls through rather than failing the scan, so a
|
||||
smudged code never costs the user their photograph.
|
||||
|
||||
**A QR carries the emitter's RUC but not its name.** Verifying a CDC against DNIT is out of
|
||||
scope for v1 (SPEC.md section 10 makes `verify_cdc` a noop), so a QR-only document shows
|
||||
`RUC 80011223` as its emitter and the keyword classifier cannot match on it: it lands in
|
||||
"Sin categoria" and the user picks one in the bandeja. Worth revisiting when CDC
|
||||
verification exists, since it is the one thing standing between a QR scan and a fully
|
||||
automatic classification.
|
||||
|
||||
### `dedupe_hash` is defined here, not in the specs
|
||||
SPEC-GAP. SPEC.md section 8 requires the column and never says what goes in it. With a CDC
|
||||
it is the CDC, which identifies a comprobante nationally. Without one it is
|
||||
`emitter | date | total | kind`, the tuple a human would use. Two photos of the same
|
||||
factura collapse; two genuinely different facturas from the same shop on the same day for
|
||||
the same amount would collapse too, which is why a merge is shown to the user ("Ya tenias
|
||||
esta factura") rather than applied silently.
|
||||
|
||||
A merge prefers QR sourced data over OCR or typing, and never overwrites a classification
|
||||
the user has already decided.
|
||||
|
||||
### Job claiming is the only dialect divergence, as specified
|
||||
`jobs/claim.ts` holds both: Postgres takes `FOR UPDATE SKIP LOCKED` inside a transaction so
|
||||
N workers take different rows; SQLite relies on its single writer and a conditional
|
||||
`UPDATE ... WHERE status = 'pending'`, where the changed-row count decides the winner. A
|
||||
test claims five queued jobs five times and asserts no id comes back twice.
|
||||
|
||||
Retries follow SPEC.md section 10 exactly (1m, 5m, 25m, 2h, 12h, then dead) and a job
|
||||
abandoned by a killed process returns to the queue once its lock is older than
|
||||
`JOBS_STALE_MINUTES`. That is the phase 3 acceptance case and it has three tests: recovery,
|
||||
the negative case of a merely slow job, and a restarted poller finishing the recovered work.
|
||||
|
||||
### OCR is structured output, not prose parsing
|
||||
The Anthropic call uses `messages.parse` with a Zod schema, so the model cannot return
|
||||
anything but the shape in RULES.md section 9 and there is no JSON to repair. Every field is
|
||||
nullable because "unreadable" is a real answer. `temperature: 0` from RULES.md is not sent:
|
||||
the current models reject sampling parameters, and constraining the output format achieves
|
||||
what the setting was there for.
|
||||
|
||||
A dead `ocr_extract` job records an ingest error; a retryable failure does not, so a
|
||||
transient API blip never shows up in the user's error list.
|
||||
|
||||
### The fixtures are decoder fixtures, not visual replicas
|
||||
SPEC-GAP against CONTRACTS.md section 4, which suggests HTML or canvas. `scripts/render-fixtures.ts`
|
||||
draws them with raw pixels and a real, decodable QR. Faithful KUDE artwork would mean
|
||||
carrying a browser or an SVG rasteriser to produce three images that nothing but a decoder
|
||||
ever reads; the emitter, date and totals live in `fixtures.ts`, which is where the tests and
|
||||
the seed read them from. A test decodes all three and asserts the third has no QR.
|
||||
|
||||
### The ZXing wasm is served from our own origin
|
||||
`zxing-wasm` fetches it from a CDN by default, which the CSP forbids and which would break
|
||||
the offline queue. A script copies it into `public/zxing` before dev and build. Headless
|
||||
Chromium has no `BarcodeDetector`, so the e2e runs exercise the ZXing path end to end,
|
||||
which is the fallback that matters most.
|
||||
|
||||
### Two defects the screens surfaced
|
||||
|
||||
**Seeded documents were dated in the future.** `seedDate` placed current-month documents on
|
||||
a fixed day of the month, so a day-8 document seeded on the 4th landed four days ahead. A
|
||||
comprobante dated in a period that has not happened corrupts every projection computed from
|
||||
it. `seedDate` now clamps to today and has its own tests.
|
||||
|
||||
**Category buttons announced their keyboard shortcut.** The 1-to-8 hints were part of each
|
||||
button's accessible name, so a screen reader read "Alimentacion 1". They are `aria-hidden`
|
||||
now with an explicit label on the button.
|
||||
|
||||
### Test isolation against a shared demo account
|
||||
The flows that confirm and reject run against Maria's bandeja, which is shared mutable
|
||||
state. They are serial and desktop only, and `bandeja.spec.ts` covers the same screen on a
|
||||
phone viewport without mutating anything. A test that creates a document varies the total
|
||||
as well as the name, because the name is deliberately not part of the dedupe key.
|
||||
|
||||
### Deferred, deliberately
|
||||
- **The scan FAB.** FLOWS.md B1 puts a persistent `[+ Escanear]` on every `(app)` screen.
|
||||
There is no tab bar to anchor it above until the dashboard lands, so scanning is reached
|
||||
from `/inicio`, `/comprobantes` and the empty bandeja. The FAB arrives with the app shell
|
||||
in phase 4.
|
||||
- **The offline queue.** FLOWS.md B2. It needs the service worker, which is phase 7.
|
||||
- **Auto-confirm.** The setting and its copy are live; the sweep that acts on it is a job
|
||||
for phase 4.
|
||||
- **Seeded declarations.** CONTRACTS.md section 4 asks for a ready F120 and an approved one.
|
||||
The declarations module is phase 5 and seeds them then; phase 3 seeds the documents they
|
||||
will be computed from.
|
||||
|
||||
Reference in New Issue
Block a user