docs: add the Hermes brief for the needs-attention queue

Measured all 54 blocked notes first. Only 27 are agent work:

  24  not_duplicate      the same Vula invoices uploaded three times in
                         different groupings. Resolving these would
                         double-count real deliveries.
  22  product_matched    genuine decisions -> the agent
   9  units_convertible  pipes sold by the each, ordered by the metre.
                         The 2.5 in 'IJ Pipes 450x2.5' is the length, so
                         1 ea = 2.5 m. A conversion rule, not a judgement.
   5  po_resolved        misread order numbers -> the agent proposes only

The brief leads with the rule that governs it: a wrong product match is
worse than a block, because it applies the wrong rate to a supplier
payment. It names both real wrong matches from this data, ranks the
evidence that is actually decisive (quantity pairing first, word overlap
never), and routes every action through the API so each decision writes
an alias and compounds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-09-18 00:23:47 +02:00
parent 984b1de692
commit 8d1452ae6f

View file

@ -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": "<the slip line>",
"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 = <the note's PO>`: `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": "<slip line, verbatim>", "po_item_id": "<chosen 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": "<corrected>" }
```
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: "<verbatim>" qty, unit, product_code
chose: <PO line description> @R<rate> (PO item id)
evidence: which of §5's signals, named
alias: written | not written
rerun: promoted | still blocked (<which check>)
```
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.