The long version.
The short version is on the main page. This is the evaluation methodology, the numbers that did not fit, the failures with their diagnoses, and the delivery system underneath it. About twenty minutes.
If you are hiring for evaluation or applied AI quality, section 02 is the one. It is the part I would most want to be asked about.
How to read the numbers
I have not rounded in my own favour, and where a measurement contradicted an assumption I kept the measurement.
- [M]
- I measured it, reproducible from the code, the run databases or the test suite.
- [C]
- The client stated it from their own operational data.
- [P]
- I worked it out arithmetically from the other two.
01 · The domain, properly
Why this is not a matching problem
Three documents get collapsed by everyone outside the field. A schema says what can be sent. A guideline says what must be sent and what each field means. A mapping says how one company's format translates into another's. Automating the first is straightforward. The third is a judgement about meaning.
The example that made it concrete. The partner sends a date. The receiving format has several date fields that could plausibly take it. Which one is correct depends on a code attached to a completely different part of the message, and if that code is absent, the correct answer changes again. Column alignment does not produce that. Neither does string similarity.
The structure compounds it. In the reference rulebook for the live lane, 20 of 49 fields in scope carry more than one correct answer, up to 16 answers for a single field, each right under different conditions(Measured).
So a mapping is not a list of field pairs. It is a set of conditional rules per field, and the conditions carry most of the information.
What the pipeline does, end to end
| Stage | Owner | Detail |
|---|---|---|
| Compile the guideline documents | Deterministic, $0.00 | Four separate compilers normalise XSD, PDF, Word and JSON samples into one typed schema. 340 nodes inbound, 627 outbound(Measured) |
| Preflight | Deterministic, $0.00 | Root resolution and a reachability walk in one pass, reported in plain language, before any run can start |
| Plan the work | Deterministic, $0.00 | Recursive descent over the target tree until each job fits a token ceiling. 133 jobs covering 575 fields(Measured) |
| Align | The only AI stage | Worker pool, rate smoothing, adaptive output ceiling with an escape hatch, budget check before transport, per-job persistence |
| Validate | Deterministic | Thirteen validators, V1 to V13, before a human sees a row(Measured) |
| Review | Human | Sheet view and tree view, three-way verdict, edits stored as an overlay |
| Export | Deterministic | Same data structure the UI renders, so screen and file cannot drift |
Two properties worth calling out. One code path, not a demo path and a real path: the web console routes to the same compilers and the same aligner the command line invokes. And crash resilience by construction: every job persists the moment it completes, so a run killed at job 100 of 133 keeps 100 jobs of work and resumes.
02 · The evaluation, in full
What I built
Deterministic parsers, no LLM-as-judge anywhere in any primary measurement(Measured).
| Reference set | Content | Standing |
|---|---|---|
| Certified XML/JSON | 137 field blocks, 154 instructions, 0 parse gaps(Measured) | Parsed from a real specialist's rulebook. Immutable. Gold of record. |
| EDIFACT | 89 rules, 92 instructions, 0 parse gaps(Measured) | Parsed from a second real expert-authored specification. |
| Derived, for triangulation | 137 rules. 62 high confidence, 71 medium, 4 unresolved(Measured) | 74 of 121 fields resolved by code alone, 43 of the remaining 47 with bounded AI assistance, 4 correctly declined. Permanently labelled DERIVED. |
Eight scoring buckets, so a failure has a type rather than a score: CORRECT, NO_MAPPING_AGREE, PARTIAL, WRONG_SOURCE, MISSING, SPURIOUS, EXCLUDED_GROUP_LEVEL, OUT_OF_GOLD_SCOPE. Two lenses: strict literal comparison, and a substance lens that symmetrically strips tautological scoping conditions from both sides before comparing.
What it reported
Five full EDIFACT baselines(Measured):
| Run | Strict accuracy | Untouched rate, strict / substance | Wrong-source rate | Jobs | Spend |
|---|---|---|---|---|---|
| 4e9f3bfd | 8.9% (4/45) | 0.030 | 0.000 | 71/88 | $3.407408 |
| 6610fd75 | 6.7% (3/45) | 0.050 | 0.017 | 76/84 | $3.929304 |
| 23818d2a | 13.3% (6/45) | 0.113 | 0.000 | 78/84 | $4.716567 |
| 82fba4fb | 8.9% | 0.080 / 0.360 | 0.020 / 0.080 | 83/84 | $4.394702 |
| acd9e708 | 11.1% | 0.089 / 0.286 | 0.018 / 0.036 | 83/84 | not recorded |
The pre-registered exit bar, written down before the runs: untouched-row rate at or above 0.50, wrong-source rate exactly 0, unrepairable condition-grammar failures exactly 0. On the final complete like-for-like comparison it was met on 0 of 3 terms(Measured).
XML/JSON lane, first full inbound baseline: 6.8% strict, 13.0% substance(Measured).
Finding one: my scorer had a bug that reported zero
The first implementation keyed gold by target node in a dictionary. Because 20 of 49 fields carry multiple gold rules, that dictionary silently collapsed them to one arbitrary row by last-write-wins, and reported a false 0% / 0%(Measured).
I found it, rewrote scoring to evaluate every individual gold row, and rescored offline from already-collected proposals at $0.00 additional spend(Measured).
The quieter lesson: I could rescore historical runs offline and for free because every proposal had been persisted with full provenance. I had not stored it for that reason. I stored it because keeping the full record costs nothing and discarding it is irreversible.
Finding two: the denominator was structurally unsatisfiable
The generation design proposes one answer per field. The reference data carries up to 16 answers per field, discriminated by conditions(Measured). One answer can never satisfy all sixteen. The metric was penalising a shape mismatch between the output contract and the reference contract, not an error.
I recorded it as a design finding for a future rule-DSL runtime rather than adjusting the metric. A test you have quietly reshaped to fit your own output is not a test.
Finding three: the reference data could not adjudicate
| Scope | Agreed | Disagreed | Gold settled | Needed a human |
|---|---|---|---|---|
| First pass | 35 | 42 | 1 · 2.4% | 41 · 97.6% |
| Wider scope | 81 | 2 · 2.5% | 79 · 97.5% |
Category breakdown of the first 42: supports model A 0, supports model B 1, supports both 7, supports neither 5, silent 29(Measured).
In roughly 97.5% of real disagreements the reference specification had nothing to say. Not because it was poor work, but because a mapping specification records one expert's defensible choice on one day and does not enumerate the alternatives that expert rejected.
So "disagrees with the reference" cannot be read as "wrong." The accuracy metric was measuring stylistic convergence with one analyst. Every figure in the baseline table above is a real measurement of the wrong thing.
What I replaced it with
Five deterministic oracles. No reference mapping, no model, no expert. 811 of 813 passed(Measured).
| Oracle | Checks | Result(Measured) |
|---|---|---|
| O1 | Output parses against the contract | 333 / 333 |
| O2 | Target node resolves against the compiled schema | 333 / 333 |
| O3 | Source pointer resolves, on mapped rows | 91 / 91 |
| O4 | Code-list membership | 0 / 0 · vacuous, no constant operations in this response shape |
| O5 | Mandatory-node coverage | 54 / 56 · both failures are pure container nodes |
I report O4 as vacuous rather than dropping it, and O5 as 54 of 56 rather than rounding up. A set of oracles curated for presentation is the same failure as a metric reshaped to fit the output.
I also split the buckets honestly. One baseline reported 90 SPURIOUS rows; most were regions the reference document had never covered, so the system had answered a question the reference was never asked. Those moved to OUT_OF_GOLD_SCOPE and out of scoring. Same data, honest denominator, uglier bucket counts, more truthful metric.
Abstention as a first-class output
The AI's instructions carry one rule that is the whole thesis, written as a format requirement:
If there is genuinely no matching source field, still emit a row, marked no_mapping, with a reason code. Silent omission is a defect, checked by the completeness validator.On the largest production run, of 583 rules covering 575 fields: 204 carried a mapping, 379 declared no match with a reason, and nothing was silently dropped(Measured). Confidence split 200 high, 383 medium.
One decision deliberately left open
The faster model became the live default under a written rule, with a pre-registered rollback trigger that remains armed pending human adjudication of 123 disagreement rows(Measured). It has not been disarmed because the evidence that would justify disarming it does not exist yet.
03 · The agent contract
System prompt architecture · cached prefix plus per-job payload
Code enforcement is half of the boundary. The other half is stated to the model, so the two cannot drift apart.
| Part | Contents | Cached |
|---|---|---|
| A | Role, and nine non-negotiable ground rules | yes |
| B | A six-step reasoning procedure to run per field | yes |
| C | The partner's guideline schema, as JSON | yes |
| D | The receiving structure and the operation DSL, all six operation shapes | yes |
| E | Output format specification. Strict JSON, no prose. | yes |
| E2 | Worked examples, extracted from the parsed gold standard by code | yes |
| F | The job: one segment occurrence in full detail | per call |
Four of the nine ground rules restate the boundary in the model's own language: never invent a field, element or code not in the evidence; the receiving structure is fixed; targets are IDs taken exactly from part F, so the model never writes a path string; strict structured output only.
Two more are the ones I would point an evaluation team at:
- If ambiguous, record the runner-up at lower confidence. Disagreement is data. Discarding the second-best answer discards the only evidence the first one was close.
- Never mark a proposal high-confidence without stating the evidence.
04 · Failure analysis
Rate limits were not my fault, and I tested that rather than assuming it
Early runs lost 17 of 88 jobs to HTTP 429(Measured). The tempting diagnosis is your own concurrency. At concurrency 1, with zero contention from my own side, 5 of 30 jobs, 16.7%, still hit 429(Measured). External and bursty.
Mitigation: a shared token bucket smoothing submissions to a measured 6 requests a minute, a bounded worker pool, and a sweep-and-retry pass recovering 429-failed jobs at the end of a run rather than losing them. Result on the full production run: 133 of 133 jobs, 0 not attempted(Measured).
Runaway generation, and a counterintuitive correlate
Worst observed: one job at 252.2 s, another riding the full 65,536-token ceiling for 227.3 s(Measured). Job size did not correlate. The three largest jobs in the plan consistently completed fast and clean.
The actual correlate was conditional complexity(Measured): an either-or rule spanning two source sections, or a qualifier judgement grounded in absent or corrupted evidence. The model was not overloaded. It was stuck, and had no way to notice.
C = M × 1.25, rounded up to the next 1024, floor 8192
no brake applied if C ≥ 32768
where M = the largest successful output that model has ever producedPlus a one-shot uncapped retry, so a legitimately long answer can never be permanently lost to the brake. Post-fix, four ceiling-slam attempts each died in 64.2 to 83.0 s and self-recovered(Measured). Worst case fell from 252.2 s to 83.0 s.
Degenerate output, and calibrating a detector against real data
One instruction returned a 27-character unit repeated to 13,239 characters, with a stray NUL byte that crashed the Excel writer(Measured). Two layers: at generation, detect a 1 to 64 character unit repeating ten or more times and re-roll once; at render, strip illegal control characters and truncate any cell at 2,000 characters.
The threshold matters more than the mechanism. The longest legitimate instruction anywhere in that run was 77 characters, one outlier in 421 rows(Measured), so the detector cannot plausibly fire on real content. I wrote negative tests proving six ordinary short constants are never flagged.
Optimisations that were real on a probe and false at scale
| Change | Small probe | Full scale | Outcome |
|---|---|---|---|
| Thinking-budget reduction | 1.9451× faster | 1.1128× faster | Reverted |
| Concurrency increase | Clean, zero 429s | Failed the pre-registered bar, repeatedly | Abandoned |
| Model swap | 5.76× / 8.15× by arm | 8.52× confirmed | Adopted |
The thinking budget turned out to be a soft target the model routinely exceeded, not a hard ceiling: a 95% win on six jobs, an 11% win on thirty(Measured). I ran that cycle four times before writing it down as a rule.
05 · Cost engineering
Fixed credit budget, hard rule that it would never touch paid billing. Budget ceiling checked before the transport layer, per-run and cumulative caps configurable per model, a mid-run tripwire, a cooperative stop that never interrupts an in-flight job but persists every remaining one with a visible cancelled_by_user reason, and a complete per-call ledger.
The full outbound production run, from the run database(Measured)
- Window
- 29 July 2026, 08:01:45Z to 08:30:13Z · 1,708 s
- Jobs
- 133 of 133 · 0 not attempted
- Model calls
- 165, including 32 retries and escape-hatch calls
- Tokens
- 812,727 in · 364,862 out
- Rules
- 583 · 204 mapped, 379 no-mapping · 575 targets
- Latency
- 2.1 s min · 30.4 s mean · 111.8 s max
- Spend
- $4.4814051 against a $6.00 hard cap
- Tripwire
- Did not fire. $2.7297024 at 67 jobs, threshold $3.00
- Effective concurrency
- 2.93 against a configured 3
Sum of call latencies 5,010.81 s against 1,708 s wall clock implies effective concurrency 2.93 against a configured 3. The scheduler was doing what it was told.
A statistical bug in my own estimator
The estimator projected a run's total from a median. Total spend is N × mean, and this workload is heavily right-skewed: mean $0.0287170 against median $0.009358, a 3.07× skew(Measured). Validated with a holdout: fit on one real run, predict a different one.
| Version | Prediction | Actual | Error | 1.5× bar |
|---|---|---|---|---|
| Median-based | $1.2447 | $4.4814051 | 3.60× low | FAIL |
| Mean-based, √N-scaled | $3.8194 | $4.4814051 | 17.3% low | PASS |
Wrong in the direction that would have quietly permitted an overspend. Both figures are pinned by a test-suite assertion so the regression cannot recur silently.
Context caching, stated precisely
The project's own presentation material cites "97.5% of input tokens served from context cache" as a general engine statistic. Computed from the run databases(Measured):
| Run set | Calls | Input tokens | Cached | Rate |
|---|---|---|---|---|
| EDIFACT baseline A | 171 | 18,399,087 | 17,990,478 | 97.78% |
| EDIFACT baseline B | 184 | 21,163,893 | 19,945,229 | 94.24% |
| XML/JSON production lane | 358 | 1,213,537 | 23,841 | 1.96% |
The 97.78% is real and lane-specific, reflecting an EDIFACT prompt architecture built around a 128,367-token shared cached prefix across 88 job occurrences. The production lane uses per-job payloads and does not benefit.
06 · The review surface, in detail
| Decision | Reasoning |
|---|---|
| Sheet view and tree view over the same run | A senior specialist's spatial memory of the guideline tree is real expertise. Discarding it would make the tool slower than what it replaces. |
Three-way verdict with a mandatory note on anything but correct | The training signal for the whole system. An optional training signal is not a training signal. |
| Corrections never auto-apply | Feedback reaches future runs only after specialist review. A system that learns unsupervised from individual corrections, where a wrong rule misroutes freight, is a liability dressed as a feature. |
| Edits are an overlay, never a mutation | Preserves every proposal-and-correction pair, which is the only thing that makes a real acceptance rate computable later. |
| Export equals screen by construction | The renderer consumes the same structure the UI renders. The deliverable is a contract and contracts do not vary by surface. |
| Failures are loud | Hard-failed jobs land in a dedicated "Not attempted" sheet with a reason string. |
An error-message standard, written down
Plain words first. Name the file and the action. Separate blocking from informational. No internal identifiers. Never phrase a system assumption as a user error. Report all structural problems in one preflight pass. Surface a post-compile summary before any spend can begin.
That last clause is a silent-failure guard: node count, section count, resolved root and schema version are shown before a run can start, so a misconfiguration that compiles to the wrong thing is visible before it costs money.
07 · Delivery governance
| Role | Owns |
|---|---|
| Me | Client conversations, domain understanding, product decisions, architecture, the AI boundary, evaluation design, every budget authorisation, every live run, every specialist session, all 205 numbered rulings |
| Planner | A high-capability model, given decisions and context, producing one detailed self-contained brief |
| Implementer | A coding agent receiving exactly one brief at a time, executing only that brief |
- Briefs are self-contained. Anything discussed only in chat is void.
- Contracts are locked. The implementer must stop and say so rather than route around a constraint that blocks it.
- A written definition of done. Test suite passes with real output pasted into the report, state file updated, ledger entry recorded, commit, push. Every task.
- Measurement wins. Any fact asserted in a brief must be computed from the repository before being written down. One brief of mine asserted 9 unresolvable types. The reachability walk proved 8. The record says 8.
- Every figure in a report must appear verbatim in the report. A file path pointing at a number is not a substitute for the number.
- An append-only ledger. 205 numbered rulings including superseded ones, marked superseded rather than deleted. History you can rewrite is not evidence.
- Cost tripwires and stop rules registered before a run, not after.
Scale, all(Measured). 947 tests passing in 368.91 s, offline, zero network, zero spend · 22,701 lines of application Python, 47,275 including tests and tooling · 205 rulings · 13 validators · 119 commits over 26 calendar days · about $34 to $35 a month recurring infrastructure · deployed on a single VM behind an HTTPS reverse proxy with a written runbook and additive-only schema migrations.
08 · What I'd do differently
Build the reference-free oracles first. Weeks on reference-based scoring before discovering it could settle 2 of 81. The oracles were cheaper, needed no expert data, and were the metrics that held. I had the ordering exactly backwards.
Get a specialist in front of it in week one. Two sessions changed the interface more than every decision I made alone, and they came late.
Stop trusting small probes sooner. Four cycles before I wrote the rule down. Two would have done.
Freeze earlier. Freezing the EDIFACT lane at 0 of 3 exit terms was right. I held on past the point the evidence justified.
Instrument the review itself. The system logs every model call and every cent. It logs analyst verdicts. It never logged review duration, which is why the review figure is client-reported rather than measured. A small change that would have made the headline number fully mine.
09 · Published limits
- No measured acceptance rate yet. The overlay design makes one computable the moment enough reviews land. Anyone quoting an acceptance percentage should be able to show you the denominator.
- Reference-based accuracy is not a metric I trust on this problem. The figures in section 02 are published because they are what the instrument reported.
- The outbound direction is unverified. Full production run complete, in analyst validation. The ledger records it as
UNVERIFIED, not passed. - Version 1 covers X12 4010 and 5010, EDIFACT D96A, D01B and D10B, XML and JSON, roughly 95% of observed partner transactions(Client-stated). CSV, fixed-length and proprietary formats are out of scope.
- The client and the integration partner are unnamed by agreement.
Back to the short version