evidence-ledger · diff
git:20260710.a1a963f to git:20260810.94439ad
5 added, 5 removed. Audit B to B.
---
name: evidence-ledger
description: "Build the deterministic evidence ledger (artifact_manifest.json + claims.json) that every other Anti-Autoresearch auditor reads. One pass inventories artifacts, derives the observability level (L0 PDF-only / L1 +LaTeX / L2 +repo+results) by fixed rule, and extracts span-anchored, hashed, checkable claims (numbers, comparisons, scope, method, baselines, citations, captions, table cells) into claims.json. An OPTIONAL additive cross-model pass ADDS span-anchored semantic claims — method, theorem statements with their assumptions, definitions, proof/derivation steps and equations, scope, baselines, conclusions, the motivation span, and reproducibility-artifact references (the proof, derivation, and structure anchors the family B/D/G auditors need) — it never invents a number, emits a finding, or computes a verdict. Run FIRST, before any audit skill. Triggers: \"build the ledger\", \"extract claims\", \"prep for integrity audit\", \"evidence ledger\", \"建证据账本\"."
argument-hint: [paper-dir | arxiv-id | pdf-path]
allowed-tools: Bash(*), Read, Write, Grep, Glob, mcp__codex__codex
---
# Evidence Ledger — the spine every auditor reads
> Infrastructure skill, **not an auditor**. It produces the *only* structure the
> auditor skills are allowed to reason over, so they don't each re-read the PDF and
> hallucinate a different table and a different list of numbers. It emits **no
> findings and no verdict** — only `artifact_manifest.json` + `claims.json`. See
> `references/integrity-forensics-contract.md` §"The pipeline" (stages [1]–[2]).
Build the ledger for: **$ARGUMENTS**
> 🔁 **Not verdict-bearing — but not a polling skill either.** The deterministic
> backbone (Steps 1–2) is a *pure function of the hashed sources*: same source bytes
> → byte-identical `claims.json`. Re-run it only when the sources change, never on a
> wall-clock timer. The only non-deterministic part is the optional enrichment pass
> (Step 3), which is additive and skippable. Do **not** wrap this skill in `/loop` /
> `/schedule` / `CronCreate`; there is no verdict to re-fire and no external event to
> wait on.
## Why this exists
Five language-model auditors each independently parsing a PDF = five different
hallucinated tables and five different number lists, none reproducible — and the
obvious dismissal, *"an LLM grading another LLM's paper is just slop."* The
structural answer is **one deterministic pass** that turns the paper into:
- `artifact_manifest.json` — what was observable; this fixes the **observability
level L**, the ceiling on every downstream finding's severity, and
- `claims.json` — a list of **span-anchored, hashed, checkable** claims
(`schemas/claims.schema.json`).
Every downstream finding must cite a `claim_id` from this ledger and quote a verbatim
span of it. **No ledger claim → no finding** (the single most important integrity
rule of the repo, enforced again by `tools/adjudicate_findings.py`). That is what
makes the difference between "a model said so" and "here is the exact sentence, its
file, and its content hash" (`DESIGN.md` §2).
## Role in the pipeline (what this skill does and does NOT do)
| Stage | Skill / tool | Emits | Judges? |
|-------|--------------|-------|:-------:|
| **[1]–[2] ledger** | **evidence-ledger (this skill)** + `tools/build_manifest.py`, `tools/build_claim_ledger.py` | `artifact_manifest.json` + `claims.json` | **No.** States *what the paper says*. |
| [3] auditors | `consistency-audit`, `citation-forensics`, `baseline-comparison-audit`, `experiment-forensics` | `<skill>.findings.json` (read the ledger; quote its spans) | Propose findings — not the verdict. |
| [3] surface | `presentation-signals` | capped-at-`minor` surface findings (auxiliary) | Never a standalone verdict. |
| [3] memo | `adversarial-case-builder` | an evidence-bound memo | No verdict weight. |
| [4] **verdict** | `tools/adjudicate_findings.py` | `report.json` + `REPORT.md` | **Yes** — the ONLY verdict, by fixed rules, no model in the loop. |
This skill is stage [1]–[2] only. It states **what the paper says**, never **whether
it is right**. The `finding.schema.json` `skill` enum technically lists
`evidence-ledger` for completeness, but this skill never writes a finding object. If
you came here for a PASS/FAIL, you want `/anti-autoresearch` (the orchestrator), not
this skill.
## Core principle
**Deterministic first; the model may only add, never invent, never judge.**
1. The numeric/citation/table backbone comes from **code** (`tools/`), not a model —
that is what makes the whole pipeline reproducible and defensible.
2. The optional LLM pass is an **additive claim-extractor**: it may add a
span-anchored *semantic* claim whose `text_span` is a verbatim substring of a
hashed source file; it may **never** introduce a number, alter an extracted value,
propose a finding, or assign a severity/verdict. The executor validates every
added span (Step 4) and rejects anything it cannot locate. Its surface is
deliberately broad — beyond method/scope/baseline it also captures **theorem
statements (with their assumptions), definitions, proof/derivation steps,
equations, conclusions, the motivation span, and reproducibility-artifact
references** — but every one of those rides on an **existing** `claims.schema.json`
`type` (`scope`/`method`/`comparison`/`artifact_ref`); enrichment widens *what
content* is anchored, never the type vocabulary, the Step-4 gate, or the
deterministic backbone.
3. **No span → no claim.** Every `text_span` in the ledger must be locatable in a
hashed source. This is the same gate the adjudicator enforces on findings, applied
one stage upstream.
## Constants
- **LEDGER_VERSION = `0.1`** — stamped into `claims.json` by `build_claim_ledger.py`; never hand-edit.
- **TAXONOMY_VERSION = `0.5`** — the ledger is **taxonomy-agnostic** (it tags no `pattern_id`); patterns are applied *post-hoc* by auditors (`references/hack-pattern-taxonomy.md`, now 46 integrity patterns A–H + 13 AIS + 2 advisory). Never tag a claim with a `pattern_id` here. v0.5 migrated the pure-style patterns to the zero-weight AIS track; the ledger stays untagged, but its **enrichment** (Step 3) now surfaces the *anchors* those auditors quote.
- **OBSERVABILITY = derived** — `L0` (PDF/text only) · `L1` (LaTeX, no results) · `L2` (repo + results). **`L3` is never emitted in v0** (we never promise reproduction). The rule is deterministic (`references/observability-levels.md`).
- **EMITS_FINDINGS = `false` · EMITS_VERDICT = `false`** — load-bearing. This skill produces a ledger, not judgments.
- **DETECT_ONLY = `true`** — never edits the audited paper; only reads sources and writes its own outputs (this is why `Edit` is absent from `allowed-tools`).
- **ENRICH = `true`** (default) — run the additive semantic pass (Step 3), which surfaces the *semantic, proof/derivation, and structure* spans the regex backbone misses (theorem statements, assumptions, definitions, proof steps, equations, conclusions, the motivation span, reproducibility-artifact references). Set `false` (or pass `— enrich: false`) to ship the deterministic backbone alone. Enrichment is **non-blocking**: if the Codex MCP is unavailable it is skipped and the deterministic ledger is the canonical output.
- **REVIEWER (enrichment only)** — model `gpt-5.6-sol`, `model_reasoning_effort: xhigh`, `sandbox: read-only`, **different model family** from the executor (`references/reviewer-independence.md` Layer 1). **CONTEXT_POLICY = fresh**: a new `mcp__codex__codex` thread per run, **never** `mcp__codex__codex-reply`. Told only source paths + the existing ledger, never the executor's opinions or any prior finding.
- **OUTPUTS** — `artifact_manifest.json`, `claims.json` (+ enrichment trace under `.aris/traces/evidence-ledger/<date>_run<NN>/` when Step 3 runs), all written into the paper directory.
> Resolve the repo root once and reuse it for every tool call:
> `ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)`. **Shell variables do
> not persist between separate Bash calls**, so Step 0 persists the resolved values
> to a `run.env`; every later block re-sources it (or run a Step's commands in one
> Bash call). Always use absolute paths.
---
## Step 0 — Resolve the input & set up the run
`$ARGUMENTS` is a **paper directory**, a **PDF path**, or an **arXiv id**. Resolve it
to one absolute `PAPER_DIR` + a stable `PAPER_ID`, extract PDF text if there is no
LaTeX, and persist the values. **Never fabricate inputs**; if no source text can be
produced, stop (a fake `claims.json` is worse than none).
```bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
ARG="$ARGUMENTS"
if [ -d "$ARG" ]; then # (a) paper directory
PAPER_DIR="$(cd "$ARG" && pwd)"; PAPER_ID="$(basename "$PAPER_DIR")"
elif [ -f "$ARG" ] && printf '%s' "$ARG" | grep -qiE '\.pdf$'; then # (b) lone PDF
PAPER_ID="$(basename "${ARG%.*}")"
PAPER_DIR="$(pwd)/.aa_work/$PAPER_ID"; mkdir -p "$PAPER_DIR"; cp "$ARG" "$PAPER_DIR/paper.pdf"
elif printf '%s' "$ARG" | grep -qE '^[0-9]{4}\.[0-9]{4,5}(v[0-9]+)?$'; then # (c) arXiv id
PAPER_ID="${ARG//./_}"
PAPER_DIR="$(pwd)/.aa_work/$PAPER_ID"; mkdir -p "$PAPER_DIR"
( curl -fsSL "https://arxiv.org/e-print/$ARG" -o "$PAPER_DIR/src.tar" \
&& tar -xf "$PAPER_DIR/src.tar" -C "$PAPER_DIR" 2>/dev/null ) \
|| curl -fsSL "https://arxiv.org/pdf/$ARG.pdf" -o "$PAPER_DIR/paper.pdf" # source preferred (better spans)
else
echo "ERROR: cannot resolve '$ARG' (need a dir, a .pdf path, or an arXiv id like 2401.01234)"; exit 1
fi
# No LaTeX? Extract PDF text now (best spans available at L0). -layout preserves table columns.
if ! find "$PAPER_DIR" -name '*.tex' -not -path '*/.aris/*' | grep -q .; then
# Deterministic primary-PDF pick (issue #11): asset-looking PDFs (figures/,
# images/, supplement*/ dirs or fig-like basenames) demoted, paper-like basenames
# promoted. A dir holding ONLY figure/supplement PDFs yields NO pick (exit 1) —
# the run then stops honestly at "no source text" instead of building the whole
# ledger from a figure PDF. Only exit 1 is tolerated; a selector CRASH must not
# silently degrade the run to the stale-.txt fallback.
PDF=$(python3 "$ROOT/tools/select_primary_pdf.py" "$PAPER_DIR"); rc=$?
[ "$rc" -le 1 ] || { echo "ERROR: select_primary_pdf.py failed (rc=$rc)"; exit 1; }
[ -n "$PDF" ] && { pdftotext -layout "$PDF" "$PAPER_DIR/paper.txt" 2>/dev/null \
|| mutool draw -F txt -o "$PAPER_DIR/paper.txt" "$PDF" 2>/dev/null \
|| python3 -c 'import sys,fitz;open(sys.argv[2],"w").write("\n".join(p.get_text() for p in fitz.open(sys.argv[1])))' "$PDF" "$PAPER_DIR/paper.txt" 2>/dev/null; }
# extraction yielded nothing but a pre-extracted *.txt exists? adopt it as the L0 source
if [ ! -s "$PAPER_DIR/paper.txt" ]; then
TXT=$(find "$PAPER_DIR" -maxdepth 2 -name '*.txt' -not -path '*/.aris/*' ! -name paper.txt | head -n1)
[ -n "$TXT" ] && cp "$TXT" "$PAPER_DIR/paper.txt"
fi
fi
mkdir -p "$PAPER_DIR/.aris/evidence-ledger"
cat > "$PAPER_DIR/.aris/evidence-ledger/run.env" <<EOF
ROOT="$ROOT"
PAPER_DIR="$PAPER_DIR"
PAPER_ID="$PAPER_ID"
EOF
echo "PAPER_DIR=$PAPER_DIR PAPER_ID=$PAPER_ID ROOT=$ROOT"
find "$PAPER_DIR" \( -name '*.tex' -o -name '*.pdf' -o -name '*.txt' \) -not -path '*/.aris/*' | sort
```
Every later Bash block begins with `source "<PAPER_DIR>/.aris/evidence-ledger/run.env"`
— substitute the absolute `PAPER_DIR` printed above.
**Validation gate.** `"$PAPER_DIR"` must now contain at least one `*.tex` **or** a
non-empty `paper.txt` (the exact L0 source Step 2 Branch B reads). A bare `*.pdf` whose
text never extracted is **not** enough — there are no spans to anchor.
**Failure handling.**
- No `*.tex` **and** no non-empty `paper.txt` (e.g. a PDF every extractor failed on) → **STOP**: report exactly what was searched; with no source text there are no spans to anchor, so there can be no ledger.
- arXiv `curl` failed (network/proxy) → **STOP**: report the exit and ask the caller for a local paper-dir or PDF. Do **not** fabricate a ledger.
- A near-empty / garbled `paper.txt` (scanned image, heavy math) — concretely, `wc -c < "$PAPER_DIR/paper.txt"` is implausibly small for the page count (rule of thumb: under ~1000 bytes for a multi-page paper) or is mostly non-alphanumeric → say so explicitly and treat the run as L0 with `confidence: low` throughout; do not silently proceed as if you had clean text.
## Step 1 — Artifact manifest + observability level (deterministic)
Inventory what is available and **derive** L by the fixed rule — a tool, not a manual
judgment. The level caps every downstream finding's severity, so it must be honest.
```bash
source "<PAPER_DIR>/.aris/evidence-ledger/run.env"
MAN_ARGS=(--paper-id "$PAPER_ID" --dir "$PAPER_DIR" --out "$PAPER_DIR/artifact_manifest.json")
[ -f "$PAPER_DIR/paper.txt" ] && MAN_ARGS+=(--pdf-text "$PAPER_DIR/paper.txt")
python3 "$ROOT/tools/build_manifest.py" "${MAN_ARGS[@]}"
# -> manifest: observability L1 (latex=1 pdf=0 bib=0 repo=False results=False) -> .../artifact_manifest.json
L=$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["observability_level"])' "$PAPER_DIR/artifact_manifest.json")
case "$L" in 0|1|2) : ;; *) echo "ERROR: unexpected level '$L' (v0 operates only at L0/L1/L2)"; exit 1;; esac
echo "L=$L" >> "$PAPER_DIR/.aris/evidence-ledger/run.env" # carry L forward to Step 2
echo "observability level = L$L"
```
The derivation rule (`references/observability-levels.md`; `build_manifest.py`
implements exactly this):
| Present | Level | Means |
|---------|:-----:|-------|
| repo (`code/`,`src/`,`repo/` or loose `*.py`/`*.ipynb`) **and** results (`*.json`/`*.csv` under `results/`,`outputs/`,`logs/`) | **L2** | code + results checkable: fake GT, self-norm, phantom results, paper↔result match |
| LaTeX present, **no** results | **L1** | source-level checks on stable spans (file:line, real cells, real `.bib`) |
| PDF / text only | **L0** | internal self-consistency, arithmetic, citation existence/context only |
**Validation gate — never over-state the level.** If you only have a PDF, `L` MUST be
`0`; do not hand `--observability-level 2` to Step 2 because a repo "exists somewhere
else." `build_manifest.py` **never** sets `repo.rerunnable: true` (no L3 in v0) — do
not edit it to true. Edge cases the rule handles correctly (**L2 requires both a repo
and result data files**): an empty `results/` dir (no `*.json`/`*.csv`) does **not**
reach L2, and a repo present **without** result files does **not** reach L2 either —
each stays at whatever the source gives (L1 if LaTeX is present, else L0).
**Failure handling.** `build_manifest.py` non-zero exit or empty/invalid JSON →
**STOP**: without a derived level you cannot legally cap severity downstream; do not
guess a level.
## Step 2 — Extract the deterministic ledger (no LLM)
The numeric/citation/table backbone comes from **code**, not a model. **LaTeX-first**
(stable spans + real line numbers); the PDF-text path is a lower-confidence fallback.
Pass the **same `L`** derived in Step 1.
**Pick the branch:** if any `*.tex` exist use Branch A; otherwise use Branch B. The
`TEX` array below makes that test explicit and survives spaces in paths.
**Branch A — LaTeX present (L1/L2; preferred):**
```bash
source "<PAPER_DIR>/.aris/evidence-ledger/run.env"
TEX=() # space-safe + deterministic: one path per line, sorted for reproducible order
while IFS= read -r f; do [ -n "$f" ] && TEX+=("$f"); done \
< <(find "$PAPER_DIR" -type f -name '*.tex' -not -path '*/.aris/*' | LC_ALL=C sort)
[ ${#TEX[@]} -gt 0 ] || { echo "no .tex found — use Branch B"; exit 1; }
python3 "$ROOT/tools/build_claim_ledger.py" --paper-id "$PAPER_ID" \
--latex "${TEX[@]}" \
--observability-level "$L" \
--out "$PAPER_DIR/claims.json"
# -> ledger: 13 claims {'caption': 1, 'citation': 3, 'number': 7, 'table_cell': 2} -> .../claims.json
```
**Branch B — no LaTeX (text-only spans; `paper.txt` from Step 0). Usually L0, but L2
when a repo + result files exist without any `.tex`, so pass the derived `$L` — never
a hardcoded `0` (the gate below asserts the ledger level equals the manifest `$L`):**
```bash
source "<PAPER_DIR>/.aris/evidence-ledger/run.env"
test -s "$PAPER_DIR/paper.txt" || { echo "ERROR: no extracted text"; exit 1; }
python3 "$ROOT/tools/build_claim_ledger.py" --paper-id "$PAPER_ID" \
--pdf-text "$PAPER_DIR/paper.txt" --observability-level "$L" \
--out "$PAPER_DIR/claims.json"
```
What it extracts (each claim carries `claim_id`, `type`, verbatim `text_span`,
`location{file,line,section}`, `evidence_anchor` = sha256 of the source text,
`extractor`, `confidence`; numeric claims also carry a parsed `value`):
| `type` | From | `extractor` | `confidence` |
|--------|------|-------------|--------------|
| `table_cell` | numbers inside `tabular` | `table_parser` | medium |
| `number` | numeric prose (`%`, points, `x`, or near a metric word) | `latex_regex` | high |
| `citation` | `\cite{...}` (keys in `refs[]`) | `latex_regex` | high |
| `scope` | scope/SOTA language (`comprehensive`, `robust`, `outperform…`, `first to`, …) | `latex_regex` | high |
| `caption` | `\caption{...}` | `latex_regex` | medium |
| (PDF path) `number` / `scope` | sentence text from `paper.txt` | `pdf_text` | **low** |
The section tracker labels each span `abstract | intro | method | experiments |
table:N | figure:N | appendix | body` (section names lowercased from `\section{...}`);
the **PDF-text path cannot track sections or lines** and labels them `unknown`.
Numeric claims carry `value{raw,normalized,unit,metric,direction,aggregation}` so the
deterministic layer can do arithmetic (delta/coherence checks) downstream **without** a
model. Omit `--generated-at` for **byte-reproducible** output (the eval harness does
this); pass `--generated-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)"` for run provenance — the
*claims* are identical either way.
**Worked example** (clean fixture `eval/fixtures/clean/sample_paper.tex` as `main.tex`)
— the stdout above, then two real claims (`location.file` mirrors the path you pass to
`--latex`):
```json
{ "claim_id": "C001", "type": "table_cell",
"text_span": "Baseline \\cite{smith2024bar} & 73.1 \\\\",
"location": {"file": "main.tex", "line": 36, "section": "table:1"},
"value": {"raw":"73.1","normalized":73.1,"unit":null,"metric":null,"direction":"unknown","aggregation":"unspecified"},
"evidence_anchor": "e6186efa…0460", "extractor": "table_parser", "confidence": "medium" }
{ "claim_id": "C003", "type": "number",
"text_span": "FooNet reaches 78.0\\% accuracy, improving from a 73.1\\% baseline to 78.0\\% accuracy, a 6.7\\% relative improvement.",
"location": {"file": "main.tex", "line": 9, "section": "abstract"},
"value": {"raw":"78.0","normalized":78.0,"unit":"%","metric":"accuracy","direction":"unknown","aggregation":"unspecified"},
"evidence_anchor": "e6186efa…0460", "extractor": "latex_regex", "confidence": "high" }
```
> **The ledger states, it does not judge.** Run the extractor on the *corrupted*
> `eval/fixtures/synthetic_corruptions/delta_inflate.tex` (abstract says "16.7%
> relative improvement") and you get an **identical 13-claim shape** — only C003's
> verbatim text changes. Spotting that 16.7% contradicts 73.1→78.0 is
> **consistency-audit**'s job (HP-DELTA-ERROR), not the ledger's; the ledger just
> captures the span faithfully.
**Validation gate.** Confirm the ledger is well-formed, the level matches, and report
the claim mix:
```bash
source "<PAPER_DIR>/.aris/evidence-ledger/run.env"
python3 - "$PAPER_DIR/claims.json" "$L" <<'PY'
import json,sys
d=json.load(open(sys.argv[1],encoding="utf-8")); L=int(sys.argv[2])
for k in ("ledger_version","paper_id","observability_level","source_files","claims"):
assert k in d, f"ledger missing top-level key: {k}"
assert d["observability_level"]==L, f"level drift: ledger={d['observability_level']} manifest={L}"
assert d["source_files"] and all(s.get("sha256") for s in d["source_files"]), "missing source hash"
for c in d["claims"]:
assert {"claim_id","type","text_span","location"} <= c.keys(), f"claim {c.get('claim_id')} missing field"
assert c["location"].get("file"), f"claim {c['claim_id']} has no location.file"
by={t:sum(1 for c in d["claims"] if c["type"]==t) for t in sorted({c['type'] for c in d['claims']})}
print(f"OK ledger L{L}: {len(d['claims'])} claims {by}")
PY
```
**Failure handling.**
- `build_claim_ledger.py` errors with "provide at least one --latex or --pdf-text" → your file glob matched nothing; re-check the branch (use Branch B when no `*.tex` were found, i.e. the `TEX` array is empty).
- **0 claims** on a paper that visibly has numbers/citations → the wrong files were passed or the `.tex` is a stub. Re-inspect inputs (read the head of the `.tex`/`.txt`) and re-run; do **not** fabricate claims. A genuinely claim-free paper is rare — ship the empty ledger only after confirming inputs.
- A single malformed `.tex` can crash the extractor (`build_claim_ledger.py` has no per-file `try/except`) → drop that one path from the `TEX` array and re-run rather than abandoning the whole paper; note the dropped file.
## Step 3 — Optional additive semantic enrichment (cross-model, fresh thread)
> Skip entirely when `ENRICH = false`. This step **adds** claims; it never edits or
> removes a deterministic claim, never adds a number, never proposes a finding.
The regex backbone has high recall on the *numeric/citation* surface but misses the
*semantic, proof/derivation, and structure* spans auditors need: the
**method-definition** span, **theorem statements with their assumptions**, explicit
- **scope** sentences, the **baseline** list, **comparison framings** — and, for the v0.4
+ **scope** sentences, the **baseline** list, **comparison framings** — and, for the
families, the spans that families **B** (argument-chain / causal-leap), **D**
(reproducibility) and **G** (proof & derivation) anchor to: **definitions**, **proof /
derivation steps**, **formulas / equations**, **stated assumptions**, load-bearing
**conclusions**, the **motivation** span, and **reproducibility-artifact references**
(does the paper ship / promise code, prompts, configs?). These feed `consistency-audit`
(`HP-METHOD-DRIFT`, `HP-THEOREM-SCOPE-DRIFT`, `HP-ARGUMENT-CHAIN-BREAK`,
`HP-CAUSAL-EVIDENCE-LEAP`), `experiment-forensics` (`HP-MISSING-REPRO-ARTIFACT`), and
`proof-derivation-forensics` (`HP-PROOF-OBLIGATION-GAP`, `HP-PROOF-CIRCULARITY`,
`HP-DERIVATION-INVALID`, `HP-SYMBOL-SEMANTIC-DRIFT`, `HP-ASSUMPTION-SMUGGLE`). Make
**one** cross-model call to *add* such span-anchored candidate claims. This is
**extraction help, not a review**: the model proposes candidate *claims* (verbatim
spans); the executor's deterministic substring gate (Step 4) decides what is admitted —
nothing here is a finding or a verdict (`references/reviewer-independence.md`).
**No new `type` vocabulary — broadened *content* on the existing schema types.** Every
new span rides on a `claims.schema.json` `type` the deterministic layer and Step 4
already allow, so the anti-hallucination gate is unchanged and `claims.json` stays
schema-valid. The mapping — *what new content the enrichment surfaces → which existing
`type` carries it → which family/pattern anchors to it*:
| New span the enrichment surfaces | Carrying `type` | Anchors for (family · pattern) |
|----------------------------------|:---------------:|--------------------------------|
| theorem / lemma / proposition **statement** (incl. its stated assumptions) | `scope` | B · `HP-THEOREM-SCOPE-DRIFT` · G · `HP-PROOF-OBLIGATION-GAP` |
| stated **assumption / hypothesis** (standalone) | `scope` | G · `HP-ASSUMPTION-SMUGGLE` |
| **definition** of a symbol / operator / construct | `method` | G · `HP-SYMBOL-SEMANTIC-DRIFT` |
| **proof step / derivation transition** (symbolic) | `method` | G · `HP-DERIVATION-INVALID`, `HP-PROOF-CIRCULARITY` |
| **formula / equation** (symbolic, non-numeric) | `method` | G · `HP-DERIVATION-INVALID`, `HP-SYMBOL-SEMANTIC-DRIFT` |
| load-bearing **conclusion** (causal / equivalence / relational) | `comparison` | B · `HP-CAUSAL-EVIDENCE-LEAP` |
| the **motivation / problem-framing** span | `scope` | B · `HP-ARGUMENT-CHAIN-BREAK` |
| **reproducibility-artifact reference** (code / prompt / config present or "will release") | `artifact_ref` | D · `HP-MISSING-REPRO-ARTIFACT` |
> These are **anchors, not findings.** The ledger never says a proof is circular, an
> assumption is smuggled, a chain is broken, or an artifact is missing — it only
> captures the verbatim span so the family-B/D/G reviewer has a `claim_id` to quote.
> The judgment stays in the auditor; the verdict stays in
> `tools/adjudicate_findings.py`. Family-G recall is highest at **L1**: equation and
> theorem-statement spans carry stable line numbers, which lets
> `proof-derivation-forensics` scaffold per-theorem anchor candidates by line window —
> so the prompt below asks the reviewer to include `line` whenever it extracts from
> LaTeX.
Set up the trace run dir and list the exact source paths to hand the reviewer:
```bash
source "<PAPER_DIR>/.aris/evidence-ledger/run.env"
DATE=$(date -u +%Y-%m-%d); TB="$PAPER_DIR/.aris/traces/evidence-ledger"; mkdir -p "$TB"
NN=$(printf "%02d" $(( $(find "$TB" -maxdepth 1 -type d -name "${DATE}_run*" 2>/dev/null | wc -l) + 1 )))
RUNDIR="$TB/${DATE}_run${NN}"; mkdir -p "$RUNDIR"
echo "RUNDIR=\"$RUNDIR\"" >> "$PAPER_DIR/.aris/evidence-ledger/run.env"
echo "RUNDIR=$RUNDIR"
python3 -c 'import json,sys;[print(s["path"]) for s in json.load(open(sys.argv[1]))["source_files"]]' "$PAPER_DIR/claims.json"
```
Call the reviewer with a **fresh `mcp__codex__codex` thread** (never `codex-reply`),
`cwd` = `PAPER_DIR`. Paste the source paths and the existing ledger's
`claim_id + text_span` list into the prompt:
```text
mcp__codex__codex:
model: gpt-5.6-sol
config: {"model_reasoning_effort": "xhigh"}
sandbox: read-only
cwd: <PAPER_DIR>
prompt: |
You are an ADDITIVE claim extractor for an evidence ledger. You are NOT a
reviewer and NOT a judge: do not assess correctness, do not propose findings, do
not assign severity or any verdict. Your ONLY job is to surface SEMANTIC claims a
regex pass misses, each anchored to a VERBATIM span of a real source file.
Source files (use these EXACT path strings in location.file):
[list the paths from claims.json -> source_files[].path]
The deterministic ledger already extracted these (do NOT duplicate them):
[paste the claim_id + text_span list from claims.json]
ADD claims ONLY of these SEVEN schema types (numbers and table cells are already
covered by the deterministic layer — do NOT emit `number` or `table_cell`, and do
NOT invent any new type string). Each type's CONTENT is broadened below to carry the
- proof/derivation + structure spans the v0.4 family-B/D/G auditors anchor to:
+ proof/derivation + structure spans the family-B/D/G auditors anchor to:
- method : the sentence(s) that DEFINE the proposed method / its key
conditions (e.g. "no test-time labels", backbone, training data);
ALSO a formal **definition** of a symbol/operator/construct, a
**proof step / derivation transition**, or a **formula/equation**
stated symbolically (for family G — copy the math VERBATIM,
including every \command, subscript, superscript, and delimiter).
- scope : an explicit scope/generality/limitation sentence the regex missed;
ALSO a **theorem/lemma/proposition statement** (you MUST include
its stated assumptions/hypotheses in the span, not just the
conclusion), a standalone **stated assumption**, or the
**motivation / problem-framing** sentence the intro rests on
(for families B and G).
- baseline : the sentence or list naming the baselines compared against.
- comparison : a sentence ASSERTING a comparison ("our method outperforms X") —
the framing, not the numbers; ALSO a load-bearing **conclusion**
that asserts a causal / equivalence / "therefore" relation
("A correlates with B, therefore A causes B") for family B.
- citation : a sentence whose citation is load-bearing for a specific claim.
- caption : a table/figure caption the extractor missed.
- artifact_ref: a reference to a named result file / table / appendix item; ALSO a
**reproducibility-artifact reference** — code/repo/prompt/config
the paper ships or promises ("code at github.com/…", "we will
release", "prompts in App. C", "hyperparameters in Table 5") —
for family D. Capture the EXACT sentence; do NOT judge whether the
artifact is sufficient, present, or fake.
HARD RULES (a violation gets your item silently dropped by the merger):
- Use ONLY the seven types above. A new/unknown type string is dropped.
- text_span MUST be copied CHARACTER-FOR-CHARACTER from the named file
(including LaTeX markup like \cite{...}, \%, \le, \alpha, $...$). If unsure it
is verbatim, OMIT it. Do NOT unescape, re-LaTeX, normalize, or "tidy" math.
- NEVER introduce, alter, or "tidy" a number. Do NOT emit a `value` field.
- For a theorem (`scope`), the span MUST include the stated assumptions, not just
the claim. For an assumption anchor, prefer the span stating the hypotheses.
- For a conclusion (`comparison`), include the inferential connective
("therefore"/"thus"/"hence"/"so") so the causal/equivalence leap is in the span.
- For an artifact_ref, capture the presence/promise sentence verbatim; the ledger
records that the reference EXISTS, it NEVER rules the artifact missing or fake.
- Prefer to include `line` when the source is LaTeX, so the proof/structure
auditors can scaffold per-theorem anchor candidates by line window.
- location.file MUST be one of the source paths above.
Output ONLY a strict JSON array (no prose, no markdown fence) of objects:
{"type":"<one of the seven types>","text_span":"<verbatim>",
"location":{"file":"<one of the listed paths>","line":<int — include when LaTeX>,
"section":"abstract|intro|method|experiments|theorem|proof|appendix|..."}}
Output [] if you find nothing new.
```
Then, using the **Write** tool, save two files into the `RUNDIR` printed above —
**substitute that literal absolute path** (the Write tool does not expand shell
variables like `$RUNDIR`): the reviewer's full response verbatim to
`<RUNDIR>/codex_raw.md` (forensic; never silently dropped) and the parsed JSON array to
`<RUNDIR>/enrichment_candidates.json` (strip any code fence; if the reviewer returned
`[]`, write `[]`).
**Failure handling (non-blocking).** If the Codex MCP hangs/stalls → re-invoke the
**same** prompt as a fresh thread (still `mcp__codex__codex`, never `codex-reply`). If
it fails twice, returns non-JSON, or the MCP is unavailable → **skip enrichment**,
write `[]` to `enrichment_candidates.json`, note the skip in the trace, and ship the
deterministic ledger. Enrichment is strictly additive and optional.
## Step 4 — Validate + merge enrichment (the anti-hallucination gate)
> **Skip this step whenever Step 3 was skipped** (`ENRICH = false`, or the reviewer was
> unavailable so no `enrichment_candidates.json` was written): no `RUNDIR` is set and
> the deterministic ledger from Step 2 is already the final output. Do **not** run the
> block below against an unset `$RUNDIR`.
When Step 3 ran, the executor validates **every** candidate before it enters the ledger
— the same "no span → no claim" discipline the adjudicator applies to findings. This
fails open (a missing/invalid candidates file leaves the deterministic ledger
untouched).
```bash
source "<PAPER_DIR>/.aris/evidence-ledger/run.env"
python3 - "$PAPER_DIR/claims.json" "$RUNDIR/enrichment_candidates.json" "$RUNDIR/enrichment_rejects.json" <<'PY'
import json, re, sys, pathlib
ledger_p, cand_p, rej_p = sys.argv[1], sys.argv[2], sys.argv[3]
ledger = json.load(open(ledger_p, encoding="utf-8"))
src = {s["path"]: s for s in ledger["source_files"]} # source_files[].sha256 = the text-hash anchor
norm = lambda t: re.sub(r"\s+", " ", t).strip() # extractor collapses newlines to spaces; match likewise
srctext = {p: norm(pathlib.Path(p).read_text(encoding="utf-8", errors="replace")) for p in src}
ALLOWED = {"method", "scope", "baseline", "comparison", "citation", "caption", "artifact_ref"} # never number/table_cell
try:
cands = json.load(open(cand_p, encoding="utf-8")); cands = cands if isinstance(cands, list) else []
except (OSError, json.JSONDecodeError):
cands = [] # fail open: no enrichment admitted
kept, rejected = [], []
for c in cands:
f = (c.get("location") or {}).get("file"); span = c.get("text_span", ""); t = c.get("type")
if t not in ALLOWED: rejected.append({"reason":"bad_type","candidate":c}); continue
if f not in srctext: rejected.append({"reason":"file_not_in_ledger","candidate":c}); continue
if not span or norm(span) not in srctext[f]: rejected.append({"reason":"span_not_verbatim","candidate":c}); continue
c.pop("value", None); c.pop("claim_id", None) # never trust a model number; we re-id below
c["evidence_anchor"] = src[f]["sha256"] # anchor to the same hash deterministic claims use
c["extractor"] = "manual"; c["confidence"] = "medium"
kept.append(c)
merged = ledger["claims"] + kept # deterministic claims keep their order/ids; enrichment appended
for i, c in enumerate(merged, 1): c["claim_id"] = f"C{i:03d}"
ledger["claims"] = merged
json.dump(ledger, open(ledger_p, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
json.dump(rejected, open(rej_p, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
print(f"enrichment: kept {len(kept)}, rejected {len(rejected)} -> {ledger_p}")
PY
```
The gate enforces, all mandatory:
- 1. **Type allow-list** — only the seven semantic types (`method`, `scope`, `baseline`, `comparison`, `citation`, `caption`, `artifact_ref`); `number`/`table_cell` are rejected (the deterministic layer owns numbers). The v0.4 proof/derivation + structure content (theorem statements, assumptions, definitions, proof steps, equations, conclusions, the motivation span, reproducibility-artifact references) rides on these **same seven** types (Step 3's mapping table), so this gate — and `claims.json`'s schema enum — are unchanged.
+ 1. **Type allow-list** — only the seven semantic types (`method`, `scope`, `baseline`, `comparison`, `citation`, `caption`, `artifact_ref`); `number`/`table_cell` are rejected (the deterministic layer owns numbers). The proof/derivation + structure content (theorem statements, assumptions, definitions, proof steps, equations, conclusions, the motivation span, reproducibility-artifact references) rides on these **same seven** types (Step 3's mapping table), so this gate — and `claims.json`'s schema enum — are unchanged.
2. **Span is verbatim** — `text_span` must be a substring of the source after whitespace-normalization (the extractor itself joins lines with single spaces, so normalize both sides). Paraphrase → reject.
3. **No invented numbers** — any `value` field is stripped; a number can only enter the ledger via Step 2.
4. **Real source** — `location.file` must be one of the ledger's `source_files`.
Re-id is append-only: deterministic claims keep their `C001…` ids; admitted
enrichment claims continue the sequence, tagged `extractor: "manual"`,
`confidence: "medium"`. The merge **never** removes or edits a deterministic claim.
`enrichment_rejects.json` lands in the trace (the `REJECT` count is expected — it is
the anchoring guard working).
**Failure handling.** Every candidate rejected (or `[]`) → acceptable; the ledger is
just the deterministic backbone. If a later self-check fails (non-contiguous ids, a
value crept in), re-run the merger from the deterministic ledger; never hand-patch
`claims.json`. If `claims.json` was already overwritten, rebuild from Step 2 then
re-merge.
## Step 5 — Self-check the ledger (it is the spine, so verify it)
Confirm `claims.json` is well-formed, the level still matches the manifest, ids are
contiguous, and every still-present source is **text-identical** to extraction time
(detect-only proof). The re-hash replicates `build_claim_ledger.py`'s **text** hash
(decode→re-encode UTF-8), not a raw-byte hash, so it matches `source_files[].sha256`;
any source that has gone missing is reported, not silently passed.
```bash
source "<PAPER_DIR>/.aris/evidence-ledger/run.env"
python3 - "$PAPER_DIR/claims.json" "$PAPER_DIR/artifact_manifest.json" <<'PY'
import json, sys, hashlib, os
L = json.load(open(sys.argv[1], encoding="utf-8")); M = json.load(open(sys.argv[2], encoding="utf-8"))
assert {"ledger_version","paper_id","observability_level","source_files","claims"} <= L.keys(), "missing top-level field"
assert L["observability_level"] == M["observability_level"], "ledger level != manifest level"
ids = [c["claim_id"] for c in L["claims"]]
assert ids == [f"C{i:03d}" for i in range(1, len(ids)+1)], "claim_ids not unique/sequential"
for c in L["claims"]:
assert {"claim_id","type","text_span","location"} <= c.keys(), f"{c.get('claim_id')} missing field"
assert c["location"].get("file"), f"{c['claim_id']} has no location.file"
def text_sha(p): # mirror build_claim_ledger.sha256_text
return hashlib.sha256(open(p, encoding="utf-8", errors="replace").read().encode("utf-8")).hexdigest()
missing = [s["path"] for s in L["source_files"] if not os.path.exists(s["path"])]
for s in L["source_files"]:
if os.path.exists(s["path"]):
assert text_sha(s["path"]) == s["sha256"], f"source changed since extraction: {s['path']}"
if missing:
print("WARNING: source(s) missing at self-check, hash unverified: " + ", ".join(missing))
by = {t: sum(1 for c in L["claims"] if c["type"]==t) for t in sorted({c["type"] for c in L["claims"]})}
print(f"== Evidence Ledger built == L{L['observability_level']} {len(ids)} claims {by} "
f"({sum(1 for c in L['claims'] if c.get('extractor')=='manual')} from enrichment)")
PY
echo "Outputs: $PAPER_DIR/artifact_manifest.json + $PAPER_DIR/claims.json"
```
A non-empty, conformant `claims.json` is the green light for the auditor fan-out. Do
**not** run any auditor, numeric check, or the adjudicator from here. **Failure
handling.** A source-hash mismatch means a file changed mid-run — rebuild from Step 1
against the current files so the anchors are honest.
## Output contract
Written into the paper directory (the paths every downstream auditor and the
orchestrator expect):
- **`artifact_manifest.json`** — `schemas/artifact_manifest.schema.json`. Records the observable inputs (hashed) and the derived **observability level** that caps all downstream severity (`repo.rerunnable` is always `false` in v0).
- - **`claims.json`** — `schemas/claims.schema.json`. **The evidence ledger:** span-anchored, hashed, deterministic backbone (+ any validated enrichment claims tagged `extractor: manual, confidence: medium`). The *only* structure auditors may reason over; `source_files[]` carry content hashes so every finding is reproducible against an immutable input. When Step 3 runs, the enrichment claims also carry the **proof/derivation + structure anchors** (theorem statements, assumptions, definitions, proof steps, equations, conclusions, the motivation span, reproducibility-artifact references) the v0.4 family-B/D/G auditors quote — all on the **existing** schema `type`s (no new vocabulary).
+ - **`claims.json`** — `schemas/claims.schema.json`. **The evidence ledger:** span-anchored, hashed, deterministic backbone (+ any validated enrichment claims tagged `extractor: manual, confidence: medium`). The *only* structure auditors may reason over; `source_files[]` carry content hashes so every finding is reproducible against an immutable input. When Step 3 runs, the enrichment claims also carry the **proof/derivation + structure anchors** (theorem statements, assumptions, definitions, proof steps, equations, conclusions, the motivation span, reproducibility-artifact references) the family-B/D/G auditors quote — all on the **existing** schema `type`s (no new vocabulary).
- **`.aris/traces/evidence-ledger/<date>_run<NN>/`** — **only when Step 3 ran**: `codex_raw.md` (raw reviewer reply), `enrichment_candidates.json` (parsed array), `enrichment_rejects.json` (the anchoring guard's rejects).
Explicitly **NOT** emitted: any `<skill>.findings.json`, any `overall_verdict`, any
`pattern_id` tagging, any accusation. Those belong to the auditors and
`tools/adjudicate_findings.py`.
## What consumes the ledger downstream (integration)
You normally reach these via `/anti-autoresearch`; the exact contracts are:
```bash
# consistency-audit's deterministic arithmetic layer (HP-DELTA-ERROR, HP-NUM-INFLATE):
python3 "$ROOT/tools/check_numeric_consistency.py" --ledger "$PAPER_DIR/claims.json" \
--out consistency-audit.deterministic.findings.json
# presentation-signals' surface checks (HP-DUP-TABLE via table_cell claims, etc.) —
# AUXILIARY, capped at minor by the adjudicator, default false_positive_risk:high,
# NOT an AI-text classifier, never a standalone verdict:
python3 "$ROOT/tools/check_presentation.py" --ledger "$PAPER_DIR/claims.json" \
--out presentation-signals.deterministic.findings.json
# the deterministic adjudicator — --ledger is REQUIRED:
python3 "$ROOT/tools/adjudicate_findings.py" --findings *.findings.json \
--ledger "$PAPER_DIR/claims.json" --paper-id "$PAPER_ID" \
--observability-level "$L" --taxonomy-version 0.5 --out report.json --md REPORT.md
```
`adjudicate_findings.py` **requires** `--ledger`: it re-verifies that each
above-`info` finding quotes a verbatim ledger span; without it every such finding
**fails closed to `info`** — a missing or wrong ledger silently neuters the whole
audit. The ledger you build here is load-bearing for every verdict. **This skill does
not run any of these** — stop at a validated ledger.
## Key rules
- **Deterministic first.** The numeric/citation/table backbone comes from code, not a model — that reproducibility is the whole credibility argument. Same source bytes → byte-identical ledger (omit `--generated-at`).
- **Spans are real.** Every `text_span` is a verbatim substring of a hashed source. The executor rejects any enrichment span it cannot match (Step 4); the adjudicator rejects unanchored findings again downstream.
- - **Enrichment adds, never invents.** Only the seven semantic types — even the broadened v0.4 proof/derivation + structure content (theorem statements, assumptions, definitions, proof steps, equations, conclusions, the motivation span, reproducibility-artifact references) rides on those same `type`s, never a new one; never a number, never an altered value, never a removed/edited deterministic claim. The numeric/citation backbone stays 100% deterministic, and the ledger captures these spans as **anchors only** — it never judges a proof, an assumption, a broken chain, or a missing artifact (that is the family-B/D/G auditors' job).
+ - **Enrichment adds, never invents.** Only the seven semantic types — even the broadened proof/derivation + structure content (theorem statements, assumptions, definitions, proof steps, equations, conclusions, the motivation span, reproducibility-artifact references) rides on those same `type`s, never a new one; never a number, never an altered value, never a removed/edited deterministic claim. The numeric/citation backbone stays 100% deterministic, and the ledger captures these spans as **anchors only** — it never judges a proof, an assumption, a broken chain, or a missing artifact (that is the family-B/D/G auditors' job).
- **Never over-state the level.** `L` is derived from the artifacts present and caps all downstream severity. A PDF-only run is L0 — full stop. Never set `repo.rerunnable: true` (no L3 in v0).
- **No judgment here.** The ledger states *what the paper says*, never *whether it is right*. `EMITS_FINDINGS = false`, `EMITS_VERDICT = false`; no `pattern_id` tagging.
- **Cross-model, fresh thread, no leakage.** The one enrichment call is `gpt-5.6-sol` @ `xhigh`, `read-only`, a *different* family from the executor, a new `mcp__codex__codex` thread (never `codex-reply`), told only source paths + the ledger.
- **Detect-only.** Never edit the audited paper; only read sources and write this skill's own outputs (Step 5's hash check proves it).
## When NOT to use (and limits)
- **Not an auditor.** No PASS/FAIL, no findings. For a verdict, run the auditor skills + `tools/adjudicate_findings.py` (or `/anti-autoresearch`).
- **Don't skip it before an audit.** Every auditor reads `claims.json`; running one without the ledger means it re-reads the PDF and hallucinates structure — exactly what this repo exists to prevent.
- **Don't loop/schedule it.** A deterministic transform, not a poller and not a verdict; re-run only when sources change.
- **Recall, not certification.** The extractor is best-effort regex: high recall on the *checkable surface*, not a guarantee every claim was found. The optional enrichment (Step 3) broadens recall to the proof/derivation + structure spans family B/D/G need, but it too is best-effort and span-gated — an uncaptured pure-symbol step simply yields no anchor (the honest outcome, not a defect; recall is materially higher at L1, where theorem/equation spans carry line numbers). Low-confidence (PDF/OCR) numbers are tagged `confidence: low` for the human and adjudicator to weight; the ledger is a foundation, not a proof of completeness.
## Review tracing
Only Step 3 (the single model call) needs a trace: its `RUNDIR`
(`.aris/traces/evidence-ledger/<date>_run<NN>/`) holds `codex_raw.md`,
`enrichment_candidates.json`, and `enrichment_rejects.json` under forensic Policy C
(fresh thread, full reply, never silently dropped — see
`references/integrity-forensics-contract.md` §"Output contract per skill"), so a later
reader can see which enrichment spans were admitted and why. When `ENRICH = false` or
enrichment is skipped (reviewer unavailable), note the skip inline and ship the
deterministic ledger — no trace dir is required. Steps 0–2 and 4–5 are deterministic;
their stdout and the hashed `source_files` are the only record needed.