diff --git a/docs/handoffs/2026-09-18-hermes-needs-attention-brief.md b/docs/handoffs/2026-09-18-hermes-needs-attention-brief.md new file mode 100644 index 0000000..97b8938 --- /dev/null +++ b/docs/handoffs/2026-09-18-hermes-needs-attention-brief.md @@ -0,0 +1,189 @@ +# Handoff brief — Hermes: clear the delivery note "needs attention" queue + +Paste this whole file as the agent's instructions. + +State at the time of writing: **34 batches — 11 processed, 23 needs attention. 79 candidate notes — 25 promoted, 54 blocked. 0 aliases recorded.** + +--- + +## 1. The one rule that matters + +**A wrong product match is worse than leaving a note blocked.** + +Every delivery line you link to a purchase order line applies that line's **rate** to money about to be paid to a supplier. A block is visible and costs a person two minutes. A wrong match silently pays the wrong amount and nobody notices. + +Two real examples from this data, both of which a plausible-looking guess gets wrong: + +| Slip line | Tempting answer | Correct answer | +| --- | --- | --- | +| `U PIPES-600 X 2.5(100D)` | `S&S Pipes-1050 X 2.5(100D)RR` R3 960.05 | `IJ PIPES-600 X 2.5(100D)` R514.30 | +| `Grip Fibreglass Measuring Tape Orange a` | `Grip Tape Measure Black/Orange 10 m` R129.13 | `Grip Fibreglass Measuring Tape ... 50000 mm` R194.87 | + +**When two purchase order lines differ only by a number — 450 vs 600 vs 1050, 10 m vs 50 m — that number IS the product.** If you cannot tell which, say so and move on. Do not pick the closest wording. + +If you are ever between two lines with different rates and no decisive evidence: **leave it blocked and report it.** That is a success, not a failure. + +## 2. What is actually blocking, and which of it is yours + +I measured all 54 blocked notes. They are four different problems, and **only two of them are yours.** + +| Blocked notes | Check | Yours? | +| --- | --- | --- | +| 24 | `not_duplicate` | **No** — see §3 | +| 22 | `product_matched` | **Yes** — §5 | +| 9 | `units_convertible` | **No** — see §4 | +| 5 | `po_resolved` | **Yes** — §6 | + +Do not attempt the 24 duplicates or the 9 unit failures. Explanations below so you can recognise and skip them. + +## 3. SKIP: the 24 `not_duplicate` notes + +These are **not** a matching problem. The same Vula invoices were uploaded three times in different groupings: + +- `vdn074329_cam_scanner_..._14.24.pdf` — one 13-page bundle holding `VCINV073787` … `VCINV074143` +- `vdn074606_megcpo9109_vula_...` — 12 of the same invoices again +- `vdn074468_megcpo9109_the_hills_ext12_vula_...` — 11 of the same again +- plus ~8 single-page files each repeating one of them + +The duplicate guard is doing its job. The right action is to discard the redundant *batches*, which a person does with the "Already ingested" button. **If you "resolve" these you will double-count real deliveries and overpay a supplier.** Leave them alone and list them in your report. + +## 4. SKIP: the 9 `units_convertible` notes + +Two patterns, both needing a conversion rule rather than a per-note decision: + +**Pipes sold by the each, ordered by the metre.** + +``` +slip: 34 ea "U PIPES-450 X 2.5(100D)" product_code P450-100 +PO: 222.5 m "IJ Pipes 450x2.5 100D" @ R339.66 per m +``` + +The `2.5` in the description is the pipe's length in metres, so 1 ea = 2.5 m and 34 ea = 85 m. That is a per-product constant, not a judgement — it belongs in a conversion table, and a developer is adding it. + +**Rebar by mass, ordered by number** (`Rebar 12mm Merchant`, `Ton → no`). Not derivable from the documents. Needs a person. + +Recognise these and skip them. Report any *other* unit pair you see so the table can cover it. + +## 5. YOUR JOB A: the 22 `product_matched` notes + +### What to read + +For each blocked note in `dn_batch_notes`: + +| Field | Use | +| --- | --- | +| `batch` | → `dn_batches` record (the source file) | +| `delivery_note_number`, `doc_type`, `doc_number_alts` | identify the document | +| `purchase_order` | → the `purchase_orders` record it resolved to | +| `items` | the slip lines: `description`, `qty`, `unit`, `unit_price`, **`product_code`** | +| `checks` | find `{"id":"product_matched","passed":false}` and read its **`detail`** array | +| `page_file` | the per-note PDF, if you need to see the paper | + +**`checks[].detail` already contains the candidate list.** Each entry is: + +```json +{ "description": "", + "kind": "ambiguous" | "none", + "candidates": [ { "id", "description", "material_code" } ] } +``` + +- `kind: "ambiguous"` — the matcher narrowed it and could not choose. Candidates are ranked, best first. +- `kind: "none"` — no text signal at all. Candidates are **every** line on that purchase order, because the right answer may look nothing like the slip text (`Kabelflex Pipe & Coupling` is genuinely `160mm Sleeves Black`). + +Cross-check against `purchase_order_items` filtered by `purchase_order = `: `description`, `qty`, `unit`, `rate`, `material_code`. + +### Evidence that is actually decisive, strongest first + +1. **Quantity pairing.** If a slip line's `qty` equals exactly one PO line's `qty`, that is near-conclusive. This settled the tape case: slip qty 1 ↔ Fibreglass PO qty 1, slip qty 3 ↔ 10 m PO qty 3. +2. **The numbers in the description must agree.** 450 ≠ 600 ≠ 1050. A shared `100D` or `2.5` proves nothing — those are the class and wall thickness, common to every line. +3. **`product_code` on the slip.** `P450-100` is stable and never truncated by the printer. If two slips carry the same code, they are the same product. +4. **Rate sanity.** A 50 m tape costing more than a 10 m one is expected. If your choice makes a small item expensive or a large one cheap, re-check. +5. **The page image** (`page_file`), when the stored text is clipped. + +### What NOT to trust + +- **Word overlap.** It is what produced both wrong matches above. +- **The stored description being complete.** Builders' forms print a ~20-character column that wraps twice then stops mid-word: `Grip Fibreglass Meas` / `uring Tape Orange a`. The truncation is on the paper. **Never invent the missing ending** and never substitute the product name you think it means. + +### How to act + +Do **not** write to PocketBase. Use the API, so your decision goes through the same validation as everything else and is remembered: + +``` +POST /api/dn-batches/{batchId}/notes/{noteId}/resolve-item +Body: { "description": "", "po_item_id": "", "global": false } +``` + +This records an **alias** keyed on the slip's product code and description. The next delivery quoting the same wording or code matches automatically — so every decision you make compounds. There are currently 0 aliases; you are building that table. + +Set `"global": true` only for a spelling convention that holds for *every* supplier (e.g. `S&S` written `SandS`). Default to `false`. + +Then re-run the checks for that batch: + +``` +POST /api/dn-batches/{batchId}/rerun +``` + +The note promotes only if it now passes every check. If it does not, report why — do not try to force it. + +## 6. YOUR JOB B: the 5 `po_resolved` notes + +The order number read off the slip matched no purchase order on file. + +``` +DQ0010051 "ME22/PP/045" — a different numbering scheme entirely +AS0002086 "MEGCP00165" — likely MEGCPO9165 misread +INA00005204 "MEGCP091507 / SRU 11987" — likely MEGCPO9150 plus a second reference +INV70963 no order number on the slip +``` + +Read every `purchase_orders` record (`purchase_order_number`, `supplier`, `site`, `total_value`) and judge whether the slip's number is a misreading of one you hold. + +**Confirm with a second signal before proposing a PO.** The supplier must match, and at least one slip line should plausibly appear on that order. A misread digit is common; guessing the wrong order links a whole delivery to the wrong budget. + +Two known traps in this data: + +- **`#MEGCP09109` exists as a purchase order and should not.** It is an OCR artefact of `MEGCPO9109` — a `#` prefix plus an `O`/`0` swap — with a different supplier (Vula Concrete vs Vula Civil Engineering) and only 2 lines. If a note resolves onto it, that is wrong. Flag it; do not build on it. +- **`IN132201` is in the purchase order table** but looks like a supplier invoice number. + +For this class, **report your proposal — do not apply it.** Correcting a PO link needs a person. Use: + +``` +PATCH /api/dn-batches/{batchId}/notes/{noteId} +Body: { "po_number_raw": "" } +``` + +only if a human has told you to. + +## 7. Collections you will touch + +| Collection | Access | Why | +| --- | --- | --- | +| `dn_batches` | read | the uploaded file, status, `checks`, `open_pages` | +| `dn_batch_notes` | read | candidate notes, `items`, per-note `checks` | +| `purchase_orders` | read | resolve and sanity-check order numbers | +| `purchase_order_items` | read | the lines you match against — `description`, `qty`, `unit`, `rate` | +| `dn_item_aliases` | **write via the API only** | your remembered decisions | +| `delivery_notes` | **never write** | the ledger. Only the pipeline promotes into it. | +| `delivery_note_items` | **never write** | same | + +Writing to `delivery_notes` directly is how the original bug lost half a delivery. Everything goes through the routes. + +## 8. Report format + +Per note you touched: + +``` +batch / note / document number + class: product_matched | po_resolved + decision: resolved | proposed | left blocked + slip line: "" qty, unit, product_code + chose: @R (PO item id) + evidence: which of §5's signals, named + alias: written | not written + rerun: promoted | still blocked () +``` + +Then totals: resolved / proposed / left blocked, plus every note you skipped and why. + +**Leaving a note blocked with a clear reason is a good outcome.** Do not inflate your resolved count. Somebody pays these invoices from these numbers.