MEGAINFRA-WEBAPP/docs/handoffs/2026-09-18-hermes-needs-attention-brief.md
DEls 8d1452ae6f 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>
2026-09-18 00:23:47 +02:00

9.5 KiB

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:

{ "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.