Skip to content

Runbook — quarantine triage

Audience: whoever is on corpus duty. Trigger: an ingest run reports N quarantined, or openregs quarantine list is not empty. Time: minutes for the decision, longer for whichever path it points at.

A document in quarantine was fetched, archived and pinned in SOURCES.lock, and then refused by one of the four validation gates (or by the OCR confidence bar its source sets). Nothing was written to the canon for it and nothing will be until you resolve it: the gates are the only way in, not a warning beside one. An item that sits in the queue is a piece of law the corpus does not contain, so the queue is worked, not watched.

Everything below is executable against the fixture corpus. Substitute your own paths for $WORK.

Terminal window
WORK=/tmp/triage # scratch: canon, state, quarantine for this walkthrough
REMOTE=$PWD/.test-remotes/quarantine-triage.git # the regime repository

Terminal window
openregs quarantine --quarantine $WORK/quarantine list
quarantine /tmp/triage/quarantine: 1 open item(s)
eur-lex--32024R0001: 32024R0001 — reference-resolution

One line per item: the item’s directory name (how every other command addresses it), the document id, the gates that refused it, and — once somebody has taken it — who claimed it and which branch its request is on. --closed lists the resolved ones instead, each with the resolution that answered it.

The item directory itself holds everything the decision needs:

filewhat it is
item.yamlsnapshot reference (uri, sha256, storage path), the per-gate report, the mapping report, and the claim and pull request once they exist
extracted-text.txtfor a scanned source only: exactly what the recogniser read

Terminal window
openregs quarantine --quarantine $WORK/quarantine claim eur-lex--32024R0001 --by <you>

Writes claim: {by, at} into item.yaml. It refuses a second claim by somebody else rather than overwriting the first — two people fixing one mapper twice is the failure this prevents.

Terminal window
openregs quarantine --quarantine $WORK/quarantine \
open eur-lex--32024R0001 --remote $REMOTE --by <you>
eur-lex--32024R0001: pull request opened — quarantine/eur-lex--32024R0001 -> …/quarantine-triage.git (c1085e58cec8), body quarantine/eur-lex--32024R0001/PULL_REQUEST.md

There is no forge in this project and none is needed: a pull request is a branch on the regime repository plus a request body, and both are ordinary git. The command clones the remote into a temporary directory (never your checkout), creates quarantine/<item>, commits the item and a generated PULL_REQUEST.md under quarantine/<item>/, pushes the branch, and records pull_request: {remote, branch, base, commit, body, opened_at} back into item.yaml. The body carries the failing check name, the full gate report with its detail lines, the mapping report and the snapshot sha256 — everything needed to decide without re-running the pipeline. Read it first with:

Terminal window
openregs quarantine --quarantine $WORK/quarantine open eur-lex--32024R0001 --dry-run

which prints the body and pushes nothing.

Review happens on the branch; a maintainer merges it once the item closes, so the history holds both the refusal and its answer.


3. Decide: fix the mapper, or hand-annotate

Section titled “3. Decide: fix the mapper, or hand-annotate”

One question decides it: where is the defect?

fix the mapperhand-annotate
the defect is inour transformthe document the publisher served
typical reportstructural-coverage names an element type; round-trip-integrity names text that vanishedreference-resolution names a citation with no target; low OCR confidence on a damaged scan
whyordinary markup the mapper has no rule for — and the same gap is silently deforming every other document that uses itno mapper can invent a target that does not exist, or read a page that cannot be read
the fix reachesevery snapshot ever captured, through openregs replaythis one document
costsa code change, review, and a replaya person’s judgement, recorded with their name

When both readings are arguable, prefer fix the mapper: one document annotated by hand is a permanent exception, and an exception that turns out to have been a mapper gap will be reproduced on every future document of the same shape.


4a. Path one — fix the mapper, then replay

Section titled “4a. Path one — fix the mapper, then replay”

The item that motivates it: fixtures/sources/gates/list-in-a-paragraph.formex.xml, whose Article 4(1) enumerates its points in a <LIST> inside the <PARAG>. The mapper read a <LIST> hanging off an <ARTICLE> and nothing else, so the list was never visited — its elements unmapped, its text absent from the output. Two gates fire, and both are the same fact:

32024R0001: captured, sha256 7de26acb1bf4, QUARANTINED — 2 of 4 gate(s) failed: round-trip-integrity, structural-coverage
  1. Fix the mapper. For this walkthrough the fix is shipped as a patch:

    Terminal window
    git apply fixtures/patches/mapper-list-in-a-paragraph.patch

    It teaches _paragraphs to read a LIST inside a PARAG through the same _points an article’s own list goes through, and takes the Formex mapper to formex-akn/1.1.0 — a minor bump, because the mapper handles something new and no existing output changes shape.

  2. Replay every snapshot the source ever captured, offline. This is the point of fixing the mapper rather than the document: the gap is repaired everywhere it applies, in one run, with no network access at all.

    Terminal window
    openregs replay --source eur-lex --offline \
    --canon $WORK/canon --state-dir $WORK/state --report $WORK/replay.yaml
    32024R0001: sha256 7de26acb1bf4, transform formex-akn/1.1.0 -> /akn/eu/act/reg/2024/1/eng@2024-06-01 (0 rewritten, 0 unchanged, 12 added)
    32025R0007: sha256 657455d6f1e8, transform formex-akn/1.1.0 -> /akn/eu/act/reg/2025/7/eng@2025-03-01 (0 rewritten, 2 unchanged)
    replay: eur-lex, 2 captures re-transformed, 0 units rewritten, 2 unchanged, 12 added

    The refused document’s twelve units are added — they never existed — while the act that was already fine is re-emitted identically and left untouched, version bump and all. A version bump is not a change.

  3. Close the item. It closes on evidence, without you asserting anything:

    Terminal window
    openregs quarantine --quarantine $WORK/quarantine close --all \
    --canon $WORK/canon --state-dir $WORK/state
    eur-lex--32024R0001: closed — fix_mapper: re-mapped under formex-akn/1.1.0: 4 gate(s) passed

    close re-reads the archived snapshot by digest, re-maps it with the transform as it stands now, re-runs all four gates, and requires the canon to actually hold the Expression that produced. Both halves are required: a mapper that could now handle the document does not by itself put the document in the corpus. --all sweeps the whole queue, which is how one mapper fix closes every item it repaired.

Run it before the fix and it tells you so, and closes nothing:

eur-lex--32024R0001: still open — the transform still refuses it: 2 of 4 gate(s) failed: round-trip-integrity, structural-coverage (under formex-akn/1.0.0); either fix the mapper and replay, or correct the document by hand with `openregs quarantine annotate`

4b. Path two — hand-annotate the one document

Section titled “4b. Path two — hand-annotate the one document”

The item that motivates it: fixtures/sources/gates/dangling-reference.formex.xml, whose Article 4(2) cites an Article 42(9) that this twelve-article act does not contain. Every word is present and every element understood, so only one gate fires:

32024R0001: captured, sha256 38290b429609, QUARANTINED — 1 of 4 gate(s) failed: reference-resolution

No mapper can repair this: the mapper is right to refuse to guess, and the target genuinely does not exist. A person decides, and signs for the decision.

  1. Start from the mapper’s own output, so you are correcting a document rather than writing one. The item names the archived bytes; map them:

    Terminal window
    openregs normalize \
    $WORK/state/snapshots/$(yq -r .snapshot.storage_path $WORK/quarantine/eur-lex--32024R0001/item.yaml) \
    --doc-id 32024R0001 --out $WORK/draft.akn.xml

    (storage_path is the sha256/aa/bb/<digest> key printed in item.yaml; any way of reading it out of that file will do.)

  2. Correct it. Here the judgement is that the printed citation addresses the incident-reporting paragraph, so the link is supplied and the words are left exactly as the publisher printed them — Akoma Ntoso separates the text of a reference from what it points at, which is what makes this annotation rather than redrafting:

    <p>Those measures shall be reviewed following any incident reported under Article 42(9).</p>
    <p>Those measures shall be reviewed following any incident reported under <ref href="#art_5__para_3">Article 42(9)</ref>.</p>

    Never edit the legal text itself. Provenance offsets index it and every atom’s quoted_text is a byte-for-byte slice of it.

  3. Admit it under the override:

    Terminal window
    openregs quarantine --quarantine $WORK/quarantine \
    annotate eur-lex--32024R0001 --akn $WORK/draft.akn.xml --by <you> --canon $WORK/canon
    eur-lex--32024R0001: /akn/eu/act/reg/2024/1/eng@2024-06-01: 12 units in eu-act-reg-2024-1/eng@2024-06-01
    method: human_authored, authored_by: <you> — recorded in every unit's metadata

    This is the only way into the canon that does not go through the four gates, and what replaces them is attribution. Every unit’s meta.yaml records:

    source_hash: 38290b4296092dbb2e9c024e09e296b45880984ed1e5e68f50e9ea05e3eb7ee9
    publisher: eur-lex
    transform_version: formex-akn/1.0.0
    method: human_authored
    authored_by: <you>

    method: human_authored is the warning label a later reader needs: re-running transform_version over the same snapshot will not reproduce this file. The transform is still recorded because it says what the annotation started from. authored_by is required whenever method is human_authored — a hand-authored canonical text with nobody’s name on it is a text nobody can be asked about (spec/schemas/unit-meta.schema.json).

    The scope is the document, not the line you touched: a maintainer vouches for the document they inspected and committed.

  4. Check the canon accepts it, exactly as CI will:

    Terminal window
    make check-canon
    PASS id-reuse 14 record(s) checked against 0 retired id(s)
    PASS sources-integrity 14 unit(s) checked against 2 pinned payload(s) in regimes/<regime>/SOURCES.lock

    The hand-authored units are ordinary canon in every other respect, and sources-integrity still traces each one to the pinned snapshot it was derived from. The override buys an exemption from the gates, never from provenance.

  5. Close the item.

    Terminal window
    openregs quarantine --quarantine $WORK/quarantine \
    close eur-lex--32024R0001 --canon $WORK/canon --state-dir $WORK/state --by <you>
    eur-lex--32024R0001: closed — hand_annotate: 12 hand-authored unit(s) of /akn/eu/act/reg/2024/1/eng@2024-06-01 admitted by <you>

    The evidence close looks for is exactly the override: units in the canon whose method is human_authored and whose source_hash is this item’s own snapshot digest. Nothing else counts, and the item cannot be closed by saying so.


  • The item directory moves to quarantine/closed/<item>/, keeping the failure report, with a resolution: block appended naming the path taken, when, by whom, the Expression and units it produced, and the evidence.

  • If the item has a pull request, the same move is committed and pushed to its branch, so the request on the remote carries its own outcome:

    2588275 quarantine: close eur-lex--32024R0001 (hand_annotate)
    c1085e5 quarantine: eur-lex/32024R0001 failed reference-resolution

    A queue that says “closed” locally while the request still asks for a decision is exactly the drift triage exists to prevent, so this is not optional; use --no-push only when the remote is genuinely unreachable, and say so in the request.

  • Merging the branch is the maintainer’s, and is what puts the record in the regime repository’s history.

Nothing is ever deleted. quarantine/closed/ is the standing answer to “why is there a hand-authored text in this corpus”, and the only place that answer lives.


  • The document is out of scope. Do not annotate it. Fix the regime’s SCOPE.yaml or the source’s discovery bounds so it is not captured, and note that in the request before closing the branch by hand.
  • A threshold is genuinely wrong for a source (a publisher whose markup this project will never fully map, a scanned series that cannot reach 100% coverage): lower it in config/sources.yaml, in a reviewed commit, with the reason written beside it — the gazette source’s coverage_percent: 95.0 is the worked example. Lowering a threshold is a judgement about a corpus. Deleting a gate is not an option.
  • The bytes are damaged (the payload no longer matches its digest): openregs verify --snapshots reports it. Re-capture rather than annotate; an item is only as good as the archive under it.
  • spec/whitespace-policy.md — what round-trip integrity compares under.
  • docs/ARCHITECTURE.md — where the gates and the quarantine sit in the ingestion plane.
  • fixtures/sources/gates/ — one seeded document per gate; the header of each says what it trips and why.

Rendered from openregs/openregs@cbe9615:docs/runbooks/quarantine-triage.md