The business caseThe problem this solves
Every batch a chemical site releases keeps an archived retain sample, and how long it must be kept, in what quantity, is set by a schedule the customer supplies. The shelf and the schedule drift apart quietly: a jar goes to a laboratory for a re-assay and never comes back while its card stays on the rack, a card is filed under a sub-lot or a rework lot number, a batch number is mis-keyed and corrected in a note nobody re-indexes, and a customer's retention terms get typed in from a draft agreement that was never signed. The register goes on printing ON-SHELF for all of it. MEASURED SETUP: clone, no pip install (there is nothing to install — requirements.txt carries no package), copy .env.example to .env or set four variables once in the repo root, and run a free floor; the free floors, the stub, the board and every published number need no key at all. WHAT IT REPLACES: opening one product's archive pack, reading every note under every shelf row to decide whether the jar is still in the slot, matching sub-lot and rework cards back to their batches by hand, adding the retention months onto the right anchor date for each pair, and listing what the shelf cannot produce.
Audience
A QA archivist working a quarter's retain reconciliation, and the QA supervisor who signs the disposal list it feeds. Every number on these pages came from one real run of this code, not from a vendor page.
The inputThe actual reconciliation pack
The corpus is 62 reconciliation pack, 0.12 MB (txt 62). It is generated because it has to be. A real retain register is a site's own quality record and carries customer names, batch genealogy and deviation numbers; no chemical company will publish one, and an anonymised one would have the notes taken out — which is where this entire job lives. Generating it also makes the answer key DERIVABLE rather than typed: the pack is planned as a structure, the text is rendered from the plan, and gold.jsonl is written from the same plan, so a label cannot disagree with the file it labels.
The corpus
- The 62 reconciliation packgenerated from a fixed seed, so no real record, person or institution appears in it.
- Where each came fromdata/SOURCES.md states where every byte came from AND what the generator costs the measurement. Every site, product, customer, batch number, retain number, shelf location, staff initial and date is invented. RSP-2026, the procedure the whole corpus is graded against, is invented and says so in its own first paragraph: it is not a pharmacopoeia chapter, not a GMP regulation, not an ICH or ISO document and not any real company's archive procedure.
Swap this folder for your own material and the kit is pointed at your reconciliation pack. That is the whole change — there is no database to migrate.
RETAIN SAMPLE RECONCILIATION
PACK RSR-0001
SITE Kettleby Works (KTB)
PRODUCT PX-4410 Polyether Diol 2000
PERIOD 2026Q1 2026-01-01 to 2026-03-31
AS AT 2026-04-06
PROCEDURE RSP-2026
1. BATCHES IN SCOPE -- made in the period, or carried in because their retention is up
BATCH MADE EXPIRY PAIR SIZE DISPOSITION
B-260104-A 2026-01-04 2028-01-04 Northmoor Coatings 4200 kg RELEASED
B-260211-B 2026-02-11 2028-02-11 stock 4800 kg RELEASED
B-260318-C 2026-03-18 2028-03-18 Northmoor Coatings 5400 kg RELEASED
B-230909-P 2023-09-09 2025-09-09 Northmoor Coatings 3600 kg RELEASED
2. RETENTION SCHEDULE ON FILE -- as supplied to the archive
PAIR ANCHOR PERIOD RETAIN LABEL FILL SUPPLIED
Northmoor Coatings EXPIRY 12 months 2 x fill 500 g 2025-11-02
stock MANUFACTURE 36 months 1 x fill 250 g 2025-06-14
3. RETAIN SHELF -- archive register as it stands at the AS AT date
RETAIN BATCH LOCATION QTY UNIT PLACED STATUS
RET-01001 B-260104-A A-12-1 1000 g 2026-01-04 ON-SHELF
note: jar opened for the OOS-2026-010 re-test on 2026-02-01; 100 g drawn and the remainder re-sealed on the shelf
RET-01004 B-260211-B A-14-2 250 g 2026-02-11 ON-SHELF
RET-01007 B-260318-C B-03-3 500 g 2026-03-18 ON-SHELF
RET-01010 B-230909-P B-07-4 1000 g 2023-09-09 ON-SHELF
4. DISPOSAL LOG -- this product, all dates
RETAIN DATE APPROVED BY AUTHORITY
(no disposals recorded for this product)
5. ARCHIVE NOTESAbridged — the file continues.
The outcomeWhat a good result looks like
One pack in, one sheet out: for every batch in scope, which retain the archive can actually produce, whether it is there, whether the schedule governing it is in force, and which of six verdicts RSP-2026 reaches — plus every orphan card on the shelf, named and left where it is.
And when it cannot
And what it does when it cannot. On the scored run 62 of 62 replies parsed and 0 stopped at the token ceiling, but 25 of 62 packs carry at least one finding the answer key contradicts. The commonest is over-caution: 10 batch findings were called SCHEDULE-NOT-SUPPLIED where a schedule was in force. The opposite error — applying a schedule nobody supplied — is 0. It also declines to match a card that is there 15 times, and attaches the wrong jar 0 times.
Where it fitsWhat did work
Every line below is a measured result from this kit's own runs, with the figure that supports it. The headline above is not softened by any of them.
- Your archive register is kept live — when a jar leaves the shelf, somebody updates the row — the free rules floor, and do not buy a call at all
The paid arm's entire significant margin is the PRESENCE reading, 61 of 62 against 42. If the register is already right about what is on the shelf, that margin is worth nothing and the two arms tie on the card match at 47 each. - You need every finding evidenced with the exact row it rests on, for an auditor — the free rules floor
It copies the row out of its own parse, so when it cites, it cites verbatim: 44 of 62 packs fully credited against the paid arm's 32, p = 0.004181. This is a significant LOSS for the paid call and it is on the same table as its win. - Jars really do leave the shelf without the register being updated, and the only record is a sentence under the row — buy the call
This is the whole case. On the jar_left_the_shelf family the paid arm is 9 of 11 packs completely right against the free floor's 4 of 11, and across the hard half of the corpus 25 of 39 against 15 of 39. - You want the orphan cards found — the free rules floor
An orphan is decidable by string comparison once a /2 or -R1 suffix comes off. The floor is 62 of 62; the paid arm is 61.
And where nothing here is good enough:
- You want the disposal list itself produced — neither — this kit will not do it
A-7. Nothing here destroys a retain or releases a batch, the schema offers no field that could express either, and src/prompt.py asserts that at import. A sample past its period is FLAGGED and stays where it is.
At a glanceHow the whole thing runs
Run once, for real, on 2026-09-11. Every figure on these pages was captured from that run — nothing is written from intent.
14 steps, grouped by the question that sends you to them rather than by build order. Each tile carries the one figure that step is about, and opens the page behind it.
Should you use this?What you bring, where it stops, and when not to use it
Before you commit an afternoon to this, these are the answers that decide it. Each one is rendered from the record it lives in — and links the page that holds it in full.
| What do I have to bring? | Replace data/corpus/*.txt with your own packs in the same five-section shape, put your archive's real schedule into data/registers.json, and write data/gold.jsonl by hand or from your own generator. ⚠︎ WHAT STOPS BEING TRUE THE MOMENT YOU DO. Corpus lens → |
| When is this the wrong choice? | Avoid: Paying per pack for a reading your own register already makes. That is the case against the best-fitting scenario (“Your archive register is kept live — when a jar leaves the shelf, somebody updates the row”). 5 scenarios scored in all, each with its own. Eval lens → |
| Where does it stop working? | A pack whose five sections are not in the printed order, or whose columns are not whitespace-separated. src/archive.py is regexes over fixed-width columns and it will return empty lists rather than guess. 6 recorded failure modes, each from a run rather than a guess. Corpus lens → |
| What was never verified? | Whether a QA archivist would draw the same line between a jar 'consumed' and a jar whose balance was re-sealed. The key draws it from the generator's plan; a real archivist reading the same sentence might disagree, and this key would score that disagreement as an error. 8 items this kit says it could not check. Eval lens → |
| Can I run this on a model I control? | Yes — any OpenAI-compatible endpoint, including one on your own hardware. The shipped adapter takes its host from BASE_URL and its model from MODEL, so nothing in src/ changes. The published figures come from 1 model on the fast tier, one provider, one key. Prompt lens → |
| And if it fits — what do I stand up? | 6 artifacts with a stated home and a stated egress, and 3 decisions each with what you provision past its ceiling — plus what was not measured. That is the next page, not this one. step 14 — Run it in your environment → |
Not asked of this kit — 2 questions: clone (a fresh clone of this kit runs with nothing fetched); judge (nothing here is graded by a model).
Last verified 2026-09-11 — r001-retain-sample. Every figure on these pages was captured from that run.
Run itHow this reaches your data
Every result on this page was produced by pure code over checked-in files, with no API key — which is why you can read the numbers before anyone spends anything.
Run this on your own data
- The pipeline, its eval harness and the runs behind every numberdeployed inside your environment, on your own model endpoints, against your own documents.
- The corpus above is the shape, not the limitit is a folder swap, and there is no database to migrate.
Checked before this shipped — A clean checkout with no key configured renders the whole board, all 62 packs, the answer key, all three free floors, the committed scored run and the adversarial arm. tools/build_corpus.py --check and evals/check_labels.py both run on a bare Python 3 with nothing installed.









