You're reading the 0.10.0 archive.

Validate and repair without losing the hard cases

In chapter 2 we extracted three supplier quotes into raw envelopes without judging any of their contents. This chapter adds the codon that does the judging: it checks every extracted value against the RFQ contract, repairs what can be repaired deterministically, and writes everything it cannot cleanly resolve into a typed exception ledger. By the end we will have two artifacts – a set of validated records and a ledger of conflicts, declines, and one deliberate judgment call – and we will have run and verified the codon against captured expected output.

Where we left off and what we're adding#

We start from the chapter-2 checkpoint. Three haiku normalize codons produced raw envelopes: envelope-aster.json has two objects, while envelope-beacon.json and envelope-cedar.json have one each. No judgment has been applied yet – the envelopes are mechanical extractions, and some of what they contain is contradictory, ambiguous, or wrong.

The fourth codon, validate-and-repair, closes that gap. It uses pi/baseten/deepseek-ai/DeepSeek-V4-Pro with continuationMode: "fresh" and onFailure: "abort". It reads the three envelopes, the hand-transcribed RFQ contract at reference/rfq-bom-rev-b.json, and the corpus lookups, and it writes validated-records.json and exception-ledger.json together, so the accepted data and the record of what went wrong are always produced as a pair.

We finish at the validate-and-repair checkpoint. It is a runtime recovery point, not a Git tag: use the runtime's rollback action for a repeat, and use the same execution and data for recovery. A comparison with a different model plan needs a fresh --start-new execution and separate output. The shipped chapters are directories with their own hanks, not checkpoint names to check out.

Before running anything, note the prerequisites: run this fixture as a non-root user with Bun, Git, ANTHROPIC_API_KEY, and BASETEN_API_KEY. The ordinary bunx/npx package entry point uses Node, so the shown recipe requires Node >=22.19.0; npx hankweave@0.10.0 is the equivalent npm launcher. A Claude Code login alone is not the documented authentication setup.

The boundary between the two stages looks like this:

FIG. 1 Where we left off and what we're adding
Read the diagram as text
Output
┌────────────────────┐       ┌──────────────────────────────┐
│ normalize          │ ───▶  │ validate/repair              │
│ raw envelopes      │       │ validated-records.json       │
│ no judgment applied│       │ typed exception ledger        │
│                    │       │ exception-ledger.json        │
└────────────────────┘       └──────────────────────────────┘
        │                              │
        └─ mechanical extraction ──────┴─ contract checks + judgment

Normalize extracts mechanically; validate-and-repair checks values against the contract and exposes judgment in a ledger. That split is also reflected in the model plan: the complete captured anchor execution used haiku for five codons and pi/baseten/deepseek-ai/DeepSeek-V4-Pro for two, with a tracked codon cost of $0.88303367. For historical context only, the 2026-09-03/04 policy-model run was reported at $0.64, and the earlier Sonnet run at approximately $2.41.

Add the validation codon#

The hank entry we are adding wires this boundary into the plan – the model, the fresh continuation, the prompt file, and the two checkpointed output files:

JSON
      "id": "validate-and-repair",
      "name": "Validate & Repair Envelopes",
      "model": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro",
      "continuationMode": "fresh",
      "description": "Reads all three envelopes plus the RFQ contract and lookups. Resolves aliases, checks substitutions, dedups Aster's two submissions, computes line extensions, and routes anything it cannot cleanly resolve -- including the planted JC-1 judgment call -- to a typed exception ledger. Continue must never mean silent omission.",
      "promptFile": "prompts/validate-and-repair.md",
      "checkpointedFiles": [
        "validated-records.json",
        "exception-ledger.json"
      ],
      "outputFiles": [
        {
          "copy": [
            "validated-records.json",
            "exception-ledger.json"
          ]
        }
      ],
      "onFailure": "abort"

Check-it: The captured execution completed the fourth codon. The current chapter capture reports four of four codons complete and a tracked codon cost of $0.28754050; provider health checks and sentinel (completion-observer) calls are separate from that tracked total.

Why silent omission breaks validation#

A line can fail to resolve, a quote can conflict with another quote, or a whole quote can require a human decision. If we continue past any of these without recording the event, clean-looking output can hide what disappeared – and downstream ranking would then treat a damaged dataset as a complete one.

The governing rule is continue must never mean silent omission. Every line, part, or whole quote that does not resolve cleanly appears in exception-ledger.json with a reason. A line marked UNRESOLVED remains represented with that status. A losing document's lines may be absent from validated-records.json after deduplication, but that decision remains visible in the ledger.

Give every outcome a named place#

The rule only works if both files have a fixed shape. validated-records.json is a JSON array with exactly these 14 fields per record:

Output
supplier_code, buyer_part_id, rfq_rev, qty, unit_price_minor, price_basis,
line_extension_minor, currency, status, ranking_eligible,
source_document_id, source_quote_id, source_line_id, source_block_id

status is QUOTED, NO_BID, UNRESOLVED, or, in the broader anchor fixture, QUARANTINED. For NO_BID, UNRESOLVED, and QUARANTINED, unit_price_minor and line_extension_minor are null, not 0 or an empty string – a missing price must never look like a cheap one.

exception-ledger.json is also a JSON array. Its standard fields are exception_id, type, supplier_code, buyer_part_id, detail, resolution, and source_ref. Judgment-call rows additionally carry judgment_call, options, tentative_resolution, and confidence, so a human reviewer can see not just what the codon did but what it declined to decide.

One more constraint before the prompt itself: the prompt forbids reading the held-out truth/ directory. It is the answer key checked after the run, not an input to the codon. We use the RFQ contract and lookup files instead.

With the shapes fixed, the prompt can tell the codon how to handle deduplication, per-line checks, the judgment call, and the output files. The excerpts below are the operative rules:

Output
Both Aster envelopes share `dedup_key: "AST|NC-RFQ-0042|B"`. The rev2 envelope's `supersedes` field names the original document. Rule: **the later `issued_at` wins**; the earlier document is superseded. Do not merge the two — pick one winner's lines to carry forward.

Regardless of which wins, this is a real conflict and must be logged: emit a `DEDUP_CONFLICT` exception citing both documents' `stated_total_minor` values (they differ by one cent — this is deliberate, not a bug you should "fix"). Only the winning document's lines proceed to Step 2.
Output
1. **Resolve `buyer_part_id_raw`.** If it already matches a `buyer_part_id` in the RFQ contract, use it as-is. Otherwise look it up in `lookups/part-aliases.json`'s `aliases` map (this resolves Cedar's OCR misread `NC-1OO5-A` → `NC-1005-A` and Beacon's substituted `BI-2002-X` → `NC-1002-A`). If a substitution applies, also check `substitutions[<raw id>].approved` — if `false` or absent, emit an `UNAPPROVED_SUBSTITUTION` exception and exclude the line from ranking; if `true` (as it is here), proceed using the resolved id and note the substitution in the record. If the id still doesn't resolve to any RFQ part, emit an `UNRESOLVED_PART` exception and mark that line `status: "UNRESOLVED"`.
2. **NO_BID.** If `status_hint` is `"NO_BID"`, set `status: "NO_BID"`, `unit_price_minor: null`, `line_extension_minor: null`, `ranking_eligible: false`, and emit a `NO_BID` exception. Never treat a NO_BID as a `$0.00` quote.
3. **Compute the line extension.** Otherwise, `line_extension_minor = qty * unit_price_minor` (integer minor units — this corpus has no fractional cases). If `price_basis.kind` is `PACK` or `BOX`, cross-check consistency using `lookups/unit-conversions.json`: `unit_price_minor` must equal `pack_price_minor / pack_size` for the matching `from_uom`. If it's inconsistent, or `price_basis.kind` has no matching entry in `unit-conversions.json` and isn't `PER_EACH`, fail closed — emit a `TOTAL_MISMATCH` (inconsistent) or `UNRESOLVED_PRICE` (unknown basis) exception and never invent a corrected price.
4. **Currency.** If `currency` is missing or not present in `quote_meta`, emit a `MISSING_CURRENCY` exception and exclude the line from ranking. (All three suppliers here use USD; this check exists for future dialects, not because it triggers now.)
5. **Rev target.** If `quote_meta.rfq_rev_target` is not `"B"`, emit a `WRONG_RFQ_REV` exception for every line of that quote and exclude them from this comparison. (Not expected to trigger for AST/BCN/CDR, but check anyway — never assume.)
Output
Compare each quote's `quote_meta.valid_until` to the RFQ contract's `award_decision_date` (`2026-02-01`). Cedar's `valid_until` is `2026-01-20` — **before** the award date. The quote is expired at the moment of award.

This is a **planted, irresolvable judgment call**, not a bug to silently fix. Whether an expired-at-award quote should be excluded entirely or kept visible-but-ineligible is a human decision. Resolve it as follows and make the judgment visible, not buried:

- Set `ranking_eligible: false` on **every** line of the expired quote, regardless of that line's own per-line status.
- Emit exactly one exception of `type: "EXPIRED_VALIDITY"` for the whole quote (one row, `buyer_part_id` covering all 5 parts pipe-joined, e.g. `NC-1001-A|NC-1002-A|NC-1003-B|NC-1004-A|NC-1005-A`), with these additional fields beyond the standard ones:
  - `judgment_call: true`
  - `options: ["exclude", "show-flagged"]`
  - `tentative_resolution: "show-flagged"` — the quote stays visible in the unified output and cannot win ranking, but a human reviewer can override this.
  - `confidence: 0.5` (deliberately uncertain — this is not a confident automatic call).

Output
**`validated-records.json`** — a JSON array, one object per line that survived Steps 1–3 (the losing Aster document's lines are dropped here, since Step 1 already logged that as a conflict), with exactly these fields:

Check-it: After a run, both validation output files exist. The chapter's artifact checker, rather than a completion observer, is the check that can establish their contents.

When two submissions claim the same spot#

The first problem the codon meets is not a bad line but a duplicate document. Aster's two envelopes share dedup_key: "AST|NC-RFQ-0042|B"; the rev2 envelope also names the original in supersedes. We group the conflict, do not merge the submissions, and apply the later issued_at rule.

The winner is doc-aster-qb-1047-rev2. The original doc-aster-qb-1047 is superseded, so only the winner's five lines proceed to the per-line checks. We still emit DEDUP_CONFLICT, including both stated totals: 604000 and 604001. The one-cent drift is deliberate and remains visible rather than being "fixed."

The captured records contain five Aster lines, all with source_document_id: "doc-aster-qb-1047-rev2".

Check-it: In the resulting validated-records.json, count five Aster records and verify that each names doc-aster-qb-1047-rev2; in the ledger, verify the two stated totals and their one-cent difference.

Five checks keep each line honest#

Once deduplication has picked a winner, Aster's surviving lines, Beacon's lines, and Cedar's lines go through the same per-line checks. We keep identity, bid status, amount, currency, and revision distinct so one repair does not hide another problem.

  1. Resolve identity. A raw part ID that is not already an RFQ part is looked up in the aliases map. This maps Cedar's OCR misread NC-1OO5-A to NC-1005-A and Beacon's BI-2002-X to NC-1002-A.
  2. Approve substitutions. If the resolved ID came from the substitutions map, its approved value must be true. Otherwise we emit UNAPPROVED_SUBSTITUTION and exclude that line from ranking.
  3. Keep unresolved parts visible. A raw ID that still does not resolve emits UNRESOLVED_PART and produces an UNRESOLVED line rather than disappearing.
  4. Keep NO_BID distinct from a price. A NO_BID line has status NO_BID, null price fields, ranking_eligible: false, and a NO_BID exception. It is never a $0.00 quote.
  5. Check amount, currency, and revision. For a quoted numeric price, line_extension_minor = qty * unit_price_minor. PACK and BOX values are cross-checked against the conversion lookup; an inconsistent conversion emits TOTAL_MISMATCH, and an unknown basis emits UNRESOLVED_PRICE. Both fail closed without inventing a price. Missing currency emits MISSING_CURRENCY and excludes the line from ranking. A quote whose rfq_rev_target is not B emits WRONG_RFQ_REV for every line and excludes those lines from comparison.

The captured records below show two of these checks landing in practice: Beacon's second line carries the canonical buyer_part_id: "NC-1002-A" after substitution, and Cedar's NO_BID line has null prices while remaining fully represented.

JSON
  {
    "supplier_code": "AST",
    "buyer_part_id": "NC-1001-A",
    "rfq_rev": "B",
    "qty": 500,
    "unit_price_minor": 410,
    "price_basis": "PER_EACH",
    "line_extension_minor": 205000,
    "currency": "USD",
    "status": "QUOTED",
    "ranking_eligible": true,
    "source_document_id": "doc-aster-qb-1047-rev2",
    "source_quote_id": "q-aster-qb-1047-rev2",
    "source_line_id": "doc-aster-qb-1047-rev2-L1",
    "source_block_id": "doc-aster-qb-1047-rev2-b1"
  },
JSON
  {
    "supplier_code": "BCN",
    "buyer_part_id": "NC-1002-A",
    "rfq_rev": "B",
    "qty": 2000,
    "unit_price_minor": 81,
    "price_basis": "PER_EACH",
    "line_extension_minor": 162000,
    "currency": "USD",
    "status": "QUOTED",
    "ranking_eligible": true,
    "source_document_id": "doc-beacon-8821",
    "source_quote_id": "q-beacon-8821",
    "source_line_id": "doc-beacon-8821-L2",
    "source_block_id": "doc-beacon-8821-b2"
  },
JSON
  {
    "supplier_code": "CDR",
    "buyer_part_id": "NC-1004-A",
    "rfq_rev": "B",
    "qty": 25,
    "unit_price_minor": null,
    "price_basis": "PER_EACH",
    "line_extension_minor": null,
    "currency": "USD",
    "status": "NO_BID",
    "ranking_eligible": false,
    "source_document_id": "doc-cedar-cw-77",
    "source_quote_id": "q-cedar-cw-77",
    "source_line_id": "doc-cedar-cw-77-L4",
    "source_block_id": "doc-cedar-cw-77-b4"
  },
  {
    "supplier_code": "CDR",
    "buyer_part_id": "NC-1005-A",
    "rfq_rev": "B",
    "qty": 5000,
    "unit_price_minor": 24,
    "price_basis": "PER_EACH",
    "line_extension_minor": 120000,
    "currency": "USD",
    "status": "QUOTED",
    "ranking_eligible": false,
    "source_document_id": "doc-cedar-cw-77",
    "source_quote_id": "q-cedar-cw-77",
    "source_line_id": "doc-cedar-cw-77-L5",
    "source_block_id": "doc-cedar-cw-77-b5"
  }
]

Check-it: Verify that Beacon's second record uses NC-1002-A and that Cedar's NC-1004-A record has unit_price_minor: null, line_extension_minor: null, and ranking_eligible: false.

Why code cannot settle the exception#

The checks above can repair an alternate part ID and reject an unknown price basis, because both have deterministic right answers. They cannot decide the business meaning of an expired quote – that is a policy choice, and the codon's job is to surface it, not settle it.

Make JC-1 visible#

Cedar's quote_meta.valid_until is 2026-01-20, before the RFQ's award_decision_date of 2026-02-01. The quote is expired at the moment of award. Whether to exclude it entirely or keep it visible-but-ineligible is a human decision.

The codon's tentative resolution is show-flagged: Cedar remains visible, but every Cedar line has ranking_eligible: false, regardless of its own per-line status. The ledger records one EXPIRED_VALIDITY row for all five parts, with judgment_call: true, options: ["exclude", "show-flagged"], tentative_resolution: "show-flagged", and confidence: 0.5. The confidence value makes the uncertainty visible; it is not a confident automatic call. The captured ledger below shows all three exceptions from this chapter, with the judgment-call row last:

JSON
[
  {
    "exception_id": "EX-01",
    "type": "DEDUP_CONFLICT",
    "supplier_code": "AST",
    "buyer_part_id": "NC-1001-A|NC-1002-A|NC-1003-B|NC-1004-A|NC-1005-A",
    "detail": "Two submissions share dedup_key AST|NC-RFQ-0042|B: doc-aster-qb-1047 (issued 2026-01-10, stated_total_minor 604000) and doc-aster-qb-1047-rev2 (issued 2026-01-14, stated_total_minor 604001). The 1-cent difference in stated totals is a known discrepancy between versions.",
    "resolution": "Selected doc-aster-qb-1047-rev2 — later issued_at and explicitly supersedes doc-aster-qb-1047. Losing document's lines dropped from validated-records.",
    "source_ref": "doc-aster-qb-1047|doc-aster-qb-1047-rev2"
  },
  {
    "exception_id": "EX-02",
    "type": "NO_BID",
    "supplier_code": "CDR",
    "buyer_part_id": "NC-1004-A",
    "detail": "Cedar declined to quote NC-1004-A (gasket sheet). status_hint=NO_BID.",
    "resolution": "Line marked status=NO_BID, unit_price_minor=null, line_extension_minor=null, ranking_eligible=false.",
    "source_ref": "doc-cedar-cw-77-b4"
  },
  {
    "exception_id": "EX-03",
    "type": "EXPIRED_VALIDITY",
    "supplier_code": "CDR",
    "buyer_part_id": "NC-1001-A|NC-1002-A|NC-1003-B|NC-1004-A|NC-1005-A",
    "detail": "Cedar quote valid_until 2026-01-20 is before the RFQ award_decision_date 2026-02-01. The quote was expired at the moment of award decision.",
    "resolution": "Quote kept visible in validated-records with all lines marked ranking_eligible=false. A human reviewer may override this flag.",
    "source_ref": "doc-cedar-cw-77",
    "judgment_call": true,
    "options": ["exclude", "show-flagged"],
    "tentative_resolution": "show-flagged",
    "confidence": 0.5
  }
]

Check-it: Confirm that EX-03 contains all four judgment-call fields and that all five Cedar records are present but ineligible for ranking, even though four have status QUOTED.

Did we get it right? – the Check-it#

Validate, run, and compare#

If you use the published fixture bundle, download hankweave-fixtures-0.10.0.tar.gz. The archive already contains one top-level hankweave-fixtures-0.10.0/ directory, so extract it in its parent and enter that directory; do not pre-create a second directory with the same name.

After extraction, change into the chapter directory chapters/ch3; the commands below are all run from there. Prepare this recipe's task-only input once, and do not pre-create its destination:

⌁ Terminal
python3 ../../verify.py prepare-data ../../quote-template-unification task-data

This task-data/ contains the digitized sources, extracted digitized data, lookups, RFQ material, source quotes, and a filtered manifest. The held-out truth, generator, and planted failure specimens remain outside it. Reuse this chapter-local directory for validation and the run:

⌁ Terminal
bunx hankweave@0.10.0 hank.json task-data --validate
bunx hankweave@0.10.0 hank.json task-data \
  --headless --start-new --execution ch3-run --max-cost 9 \
  --shim-idle-timeout 1800 --overwrite-output -o ch3-out
python3 ../../verify.py chapter 3 ch3-out

--validate does not run codons or provider health checks, so its GOOD TO RUN! result is configuration/setup validation, not a model-generation self-test or proof that the Baseten key works. Runtime startup is a separate phase. The --start-new, execution name, and output directory make this an independent attempt. For an npm launcher, replace bunx hankweave@0.10.0 with npx hankweave@0.10.0 while keeping the same local paths.

For another independent attempt, stay in chapters/ch3, reuse task-data/, and choose a new execution and output directory:

⌁ Terminal
bunx hankweave@0.10.0 hank.json task-data \
  --headless --start-new --execution ch3-run-2 --max-cost 9 \
  --shim-idle-timeout 1800 --overwrite-output -o ch3-out-2
python3 ../../verify.py chapter 3 ch3-out-2

If an execution is interrupted and you deliberately resume it, keep the same task-data/, execution, and output paths and omit --start-new; this documents the recovery layout, not a captured interruption exercise.

A passing run is not the same as a correct one, so it helps to know exactly what the checker does and does not establish. The two output files contain these seven properties. verify.py chapter 3 directly enforces the array and required-exception checks in items 1–2 (along with the envelope-level checks); items 3–7 require inspecting the output files. A checker pass is not by itself proof of the one-cent totals, JC-1 fields, Cedar flags, losing-Aster absence, or Beacon resolution.

  1. validated-records.json is a JSON array.
  2. exception-ledger.json is a JSON array containing at least DEDUP_CONFLICT, NO_BID, and EXPIRED_VALIDITY rows.
  3. DEDUP_CONFLICT cites 604000 and 604001, differing by one cent.
  4. EXPIRED_VALIDITY has the four judgment-call fields and their captured values.
  5. Every Cedar line has ranking_eligible: false.
  6. The losing Aster document's lines are absent; the winner contributes five lines.
  7. Beacon's second line resolves BI-2002-X to NC-1002-A.

The checker command reads the artifacts; the quality observer used by the broader anchor is not a substitute. It observes completion-event evidence, not the contents of validated-records.json or exception-ledger.json. The current chapter capture is a complete four-codon execution with task-only inputs and tracked codon cost $0.28754050; the captured headless console exited with code 0. It does not turn the capture into a promise that every fresh run has the same cost. Fresh golden captures for chapters 1–4 also show the new mix: haiku for the normalizes, then pi/baseten/deepseek-ai/DeepSeek-V4-Pro for chapter 3 validation and chapter 4 reconciliation.

Check-it: From chapters/ch3, run python3 ../../verify.py chapter 3 ch3-out and inspect the two output files. Treat a completion event as evidence that the run completed, not as evidence that every record is correct.

What the full anchor teaches beyond three suppliers#

So far, we have worked with three suppliers. With eight in the separate full-anchor hank, we need four more hazard rules. Its hazard-bearing input envelopes and QUARANTINED status vocabulary extend the surrounding input and status cases, but its validated-records.json records use the same 14 fields as our three-supplier contract – the schema we learned above carries over unchanged.

Four hazard rules#

  1. Low-confidence quarantine: Harbor's ocr_confidence: 0.81 and page-2 warning quarantine every page-2 line. Those lines remain visible, are never ranked or dropped, have null prices, and produce one LOW_CONFIDENCE exception.
  2. Page-split repair: The native rig (the fixture's setup program) has already joined Embar's Markdown fragments. Validation verifies the joined values and logs one PAGE_SPLIT_LINE; the quoted line remains quoted. We do not claim to perform a second join.
  3. Handwriting to notes: Iris's handwritten annotation, can do 5% if you take all 5000, never enters a line item or price. One NOTE_ROUTED exception carries it verbatim, while the quoted line remains unchanged.
  4. Prefix-split repair: Dover's raw value NC-1004-A gasket sheet is split deterministically at the exact RFQ part prefix. The line remains quoted and the mapper emits no exception for this repair.

The anchor prompt encodes these rules as follows:

Output
1. **Low-confidence quarantine (HBR)**: `harbor-quote.json` carries `ocr_confidence: 0.81` and `warnings: ["low_confidence: page 2"]`. Every line from page 2 is QUARANTINED: status `QUARANTINED`, unit price and line extension null, `ranking_eligible: false` — visible, never ranked, never dropped. Emit one `LOW_CONFIDENCE` exception naming the affected lines.
2. **Page-split repair (EMB)**: the native rig has mechanically joined `embar-quote.md`'s table fragments. The affected line retains its `source_note` about the page split and `source_pages`. Verify the joined values and LOG the upstream repair as one `PAGE_SPLIT_LINE` exception; the line stays QUOTED. Do not claim to have performed a second join or quarantine a successfully joined row.
3. **Handwriting routes to notes (IRS)**: `iris-quote.json` carries `annotations: [{type: "handwriting", text: "can do 5% if you take all 5000"}]` on NC-1005-A. Annotation content NEVER enters line items or prices — route it: one `NOTE_ROUTED` exception carrying the text verbatim; the line itself stays exactly as quoted (no 5% applied — a conditional handwritten offer is a note for a human, not a price).
4. **Prefix-split repair (DVR)**: `dover-quote.json` line L4 carries `buyer_part_id_raw: "NC-1004-A gasket sheet"` — a known upstream concatenation defect (part id glued to its description). The repair is DETERMINISTIC, not a guess: if the raw string's leading token exactly matches an RFQ buyer_part_id (`NC-1004-A` does), split there — id = the exact-matching prefix, remainder = description text. The line stays QUOTED with its stated price. This is a mapper-level silent repair: NO exception row (the truth ledger carries none for it). Only when NO leading exact-match exists does a raw id become UNRESOLVED.

The full anchor reached 41/41 exact across unified-records.csv after four live iterations, with all eight exception types routed correctly. Here, 41/41 means a 41-line file match – one header plus 40 data rows – compared line-for-line with the oracle, not 41 data records. The sequence was a real debugging progression – rig-path copy, extraction and enumeration, workload budgets, then the four hazard rules – not a second chapter-3 run.

The anchor is a separate self-contained recipe, not another name for the chapter's task-data/. Run it as a non-root user with Bun, Git, ANTHROPIC_API_KEY, and BASETEN_API_KEY; a Claude Code login alone is not this recipe's authentication contract. From anchor-hank, prepare and reuse that recipe's own directory:

⌁ Terminal
python3 ../verify.py prepare-data ../quote-template-unification task-data
bunx hankweave@0.10.0 hank.json task-data --validate
bunx hankweave@0.10.0 hank.json task-data \
  --headless --start-new --execution anchor-run --max-cost 9 \
  --shim-idle-timeout 1800 --overwrite-output -o anchor-out
python3 ../verify.py anchor anchor-out
bun rigs/render-exceptions.ts anchor-out --check && bun rigs/render-award.ts anchor-out --check

Prepare once per recipe and reuse that task-data/ for repeats; independent attempts use new execution and output paths, while recovery keeps the same data and omits --start-new. The checker and render checks are run from anchor-hank, so their ../verify.py and rigs/ paths are intentional.

Check-it: The anchor result is a 41-line (header + 40 data rows) file match, 41/41 exact, against the oracle; check that its hazard contract keeps low-confidence input visible, logs page surgery, routes handwriting verbatim, and leaves the exact prefix repair without an exception.

Three principles to take forward#

First, the exception ledger is the proof. Clean output alone cannot tell us what was dropped; ledger rows show what the codon found and how it handled it.

Second, judgment calls stay explicit. JC-1 records its options, tentative resolution, and confidence so a human reviewer can see the uncertainty. The codon does not pretend to settle a human choice.

Third, the model tier belongs at the boundary. haiku extracts fields without judgment; pi/baseten/deepseek-ai/DeepSeek-V4-Pro checks those fields against the contract and routes exceptions. Moving judgment into normalize would hide the conflict from the codon whose job is to surface it.

The codon uses onFailure: "abort": a failed run stops here. The runtime checkpoints the codon's state when it ends or the run shuts down, so files the codon already wrote are not lost. We use abort for this exercise; when retries are appropriate for your work, look up their schema in concepts/codons. The schema strip below summarizes the two relevant fields:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
onFailureenumnoabort | retry | ignoreHow to handle codon failure. 'abort' (default): Use existing failure behavior (server stays active for retriable errors, shuts down for non-retriable). 'retry': Automatically retry up to maxAttempts times if the error i…
retryConfigobjectnoConfiguration for retry behavior. Only used when onFailure is 'retry'. Delays grow exponentially from delayMs and are capped at maxDelayMs; when the provider supplies a Retry-After hint, that value is used instead of th…

Our codon selects onFailure: "abort". Use concepts/codons when you are ready to configure retries.

Since 0.7.3, headless mode fails fast on a retriable failure under onFailure: "abort" instead of waiting for an interactive client. That matters for CI; this tutorial does not configure retries.

To repeat the chapter lesson, use the runtime rollback action at the validate-and-repair checkpoint and keep the same chapters/ch3/task-data/ data path. For a fresh independent comparison, use --start-new with a new execution and output path, as in the command above. Recovery reuses the same execution and data without --start-new; no Git tag or unperformed interruption capture is implied.

Check-it: The artifact checker passes only when both validation outputs satisfy the chapter contract; a run-completed event alone is not enough.