MEGAINFRA-WEBAPP/.superpowers/sdd/2026-09-18-hybrid-delivery-note-ingestion/task-6-report.md
2026-09-19 07:49:51 +02:00

5.4 KiB

Task 6 report: site-scoped supplier and PO resolution

Status: complete

Files

  • src/lib/dn-pipeline/resolve-entities.ts — pure ordered resolver and candidate/evidence types.
  • src/lib/dn-pipeline/resolve-entities.test.ts — resolver safety tests.
  • src/lib/dn-pipeline/run.ts — site-scoped PO adapter, supplier/alias loading, deterministic candidate persistence, and fuzzy-review gate.
  • src/lib/dn-pipeline/run.test.ts — updated legacy-runner expectation so fuzzy suggestions remain blocked review candidates.
  • src/lib/po-matching.ts — exported ranked PO evidence while preserving the existing findBestPoMatch API.

Decisions

  • The batch's confirmed site is authoritative. The runner queries purchase_orders with pb.filter("site = {:site}", { site: batch.site }) (with a JSON-escaped fallback for lightweight SDK doubles) and requests the real site, supplier_ref, and PO fields.
  • resolveEntities is pure and consumes already scoped rows. A unique normalised PO is authoritative and derives supplier_ref, even when the printed supplier is blank. Registration, approved document alias, and exact supplier-name evidence are deterministic supplier paths.
  • Fuzzy PO candidates are ranked with score, rank, threshold, and winner margin, but never auto-selected. The legacy runner blocks a printed-PO note with no deterministic selected PO even if a stale/mock check list says it passed.
  • findBestPoMatch remains available to existing callers. rankPoMatches exposes all scored candidates and preserves legacy first-on-tie behavior for findBestPoMatch.
  • Supplier and supplier-alias collections are additive adapters. If an older PocketBase lacks them, the runner keeps PO free-text provenance and never widens the site-scoped PO query.
  • Receipt/invoice payable semantics and forbidden clock/image-quality/nearest- site paths were not changed. No production PocketBase writes were made.

RED evidence

Command:

npx vitest run src/lib/dn-pipeline/resolve-entities.test.ts

Result before implementation: failed during collection because ./resolve-entities did not exist (Failed to load url ... Does the file exist?; Test Files 1 failed, Tests 0).

GREEN evidence

Resolver and regression command:

npx vitest run src/lib/dn-pipeline/resolve-entities.test.ts src/lib/po-matching.test.ts src/lib/dn-pipeline

Result: Test Files 14 passed (14), Tests 205 passed (205).

The runner test now explicitly proves a near PO is persisted as match_method=po_fuzzy, with no selected PO and no promotion.

Typecheck:

npm run typecheck

Result: passed (tsc --noEmit).

Diff check:

git diff --check

Result: passed.

Self-review

  • Required resolver cases are covered: blank supplier with exact PO, cross-site exclusion, approved alias, ambiguous fuzzy candidates, and zero winner margin.
  • Exact PO selection is the only path that returns a purchase-order id from this runner; fuzzy evidence remains review-only.
  • Candidate rows persist supplier, supplier_name_raw, match_method, and JSON match_evidence, while the existing checks, receipt, invoice, and promotion paths remain otherwise unchanged.
  • The only expected test stderr is the existing deliberate promotion-failure fixture ([dn-run] promote failed); it is asserted by the existing test.
  • No production PocketBase writes were made.

Commit

Implementation commit: 75a1d85c875c43f66232aaec448ff6fa1f904523.

Fix-round implementation commit: 33be75b82153cc602aa7838942ea670588cf920c.

Fix Round 1

Review findings addressed:

  • NoteSegment now carries optional supplier name and registration evidence; segmentPages copies it from the first page and fills missing values from continuations. The legacy runner loads entity data whenever any note has a PO, supplier name, or registration evidence.
  • Site-scoped PO loading now projects only real fields, including processing_status and excluding the nonexistent status; the runner test asserts the bound site filter and projection.
  • The staged schema migration additively/idempotently adds optional supplier registration_number and vat_number fields. SupplierRecord, the pure resolver type, and the loader use those exact fields. No migration was applied.
  • Fuzzy candidates are capped at 20 but retain score, threshold, rank, and margin even below threshold; they remain suggestions and never auto-resolve. The resolver now uses the exported rankPoMatches primitive.

Fix-round RED command:

npx vitest run src/lib/dn-pipeline/resolve-entities.test.ts src/lib/dn-pipeline/segment.test.ts src/lib/dn-pipeline/run.test.ts

Result before fixes: 5 failed, 52 passed. Failures covered dropped segment fields, the invalid status projection, skipped supplier-only loading, and discarded below-threshold fuzzy evidence.

Fix-round GREEN and regression commands:

npx vitest run src/lib/dn-pipeline/resolve-entities.test.ts src/lib/po-matching.test.ts src/lib/dn-pipeline

Result: Test Files 14 passed (14), Tests 210 passed (210).

python3 scripts/pb-add-dn-staged-ingestion.py --self-test

Result: self-test: index replacement and idempotence passed.

python3 scripts/pb-add-dn-staged-ingestion.py --dry-run

Result: offline preview completed with DRY RUN. No schema changes written.

npm run typecheck
git diff --check

Both passed. No production PocketBase writes were made.