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.
WORK=/tmp/triage # scratch: canon, state, quarantine for this walkthroughREMOTE=$PWD/.test-remotes/quarantine-triage.git # the regime repository0. See the queue
Section titled “0. See the queue”openregs quarantine --quarantine $WORK/quarantine listquarantine /tmp/triage/quarantine: 1 open item(s) eur-lex--32024R0001: 32024R0001 — reference-resolutionOne 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:
| file | what it is |
|---|---|
item.yaml | snapshot reference (uri, sha256, storage path), the per-gate report, the mapping report, and the claim and pull request once they exist |
extracted-text.txt | for a scanned source only: exactly what the recogniser read |
1. Claim it
Section titled “1. Claim it”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.
2. Open the pull request
Section titled “2. Open the pull request”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.mdThere 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:
openregs quarantine --quarantine $WORK/quarantine open eur-lex--32024R0001 --dry-runwhich 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 mapper | hand-annotate | |
|---|---|---|
| the defect is in | our transform | the document the publisher served |
| typical report | structural-coverage names an element type; round-trip-integrity names text that vanished | reference-resolution names a citation with no target; low OCR confidence on a damaged scan |
| why | ordinary markup the mapper has no rule for — and the same gap is silently deforming every other document that uses it | no mapper can invent a target that does not exist, or read a page that cannot be read |
| the fix reaches | every snapshot ever captured, through openregs replay | this one document |
| costs | a code change, review, and a replay | a 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-
Fix the mapper. For this walkthrough the fix is shipped as a patch:
Terminal window git apply fixtures/patches/mapper-list-in-a-paragraph.patchIt teaches
_paragraphsto read aLISTinside aPARAGthrough the same_pointsan article’s own list goes through, and takes the Formex mapper toformex-akn/1.1.0— a minor bump, because the mapper handles something new and no existing output changes shape. -
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.yaml32024R0001: 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 addedThe 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.
-
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/stateeur-lex--32024R0001: closed — fix_mapper: re-mapped under formex-akn/1.1.0: 4 gate(s) passedclosere-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.--allsweeps 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-resolutionNo 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.
-
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_pathis thesha256/aa/bb/<digest>key printed initem.yaml; any way of reading it out of that file will do.) -
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_textis a byte-for-byte slice of it. -
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/canoneur-lex--32024R0001: /akn/eu/act/reg/2024/1/eng@2024-06-01: 12 units in eu-act-reg-2024-1/eng@2024-06-01method: human_authored, authored_by: <you> — recorded in every unit's metadataThis 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.yamlrecords:source_hash: 38290b4296092dbb2e9c024e09e296b45880984ed1e5e68f50e9ea05e3eb7ee9publisher: eur-lextransform_version: formex-akn/1.0.0method: human_authoredauthored_by: <you>method: human_authoredis the warning label a later reader needs: re-runningtransform_versionover the same snapshot will not reproduce this file. The transform is still recorded because it says what the annotation started from.authored_byis required whenevermethodishuman_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.
-
Check the canon accepts it, exactly as CI will:
Terminal window make check-canonPASS 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.lockThe hand-authored units are ordinary canon in every other respect, and
sources-integritystill traces each one to the pinned snapshot it was derived from. The override buys an exemption from the gates, never from provenance. -
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
closelooks for is exactly the override: units in the canon whosemethodishuman_authoredand whosesource_hashis this item’s own snapshot digest. Nothing else counts, and the item cannot be closed by saying so.
5. What closing does
Section titled “5. What closing does”-
The item directory moves to
quarantine/closed/<item>/, keeping the failure report, with aresolution: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-resolutionA 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-pushonly 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.
6. If neither path applies
Section titled “6. If neither path applies”- The document is out of scope. Do not annotate it. Fix the regime’s
SCOPE.yamlor 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 — thegazettesource’scoverage_percent: 95.0is 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 --snapshotsreports it. Re-capture rather than annotate; an item is only as good as the archive under it.
See also
Section titled “See also”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