# ARS v3.10 — Issue #182 Promote Deterministic Citation Verification Gate

**Date:** 2026-05-21
**Issue:** [#182](https://github.com/Imbad0202/academic-research-skills/issues/182)
**Umbrella:** [#198](https://github.com/Imbad0202/academic-research-skills/issues/198) (v3.10 deterministic verification layer)
**Target release:** v3.10
**Companion specs:** `2026-05-21-v3.10-183-epistemic-status-spec.md` (full, the source of truth for the §3.6 `verification_gate` shared contract), `2026-05-21-v3.10-184-extend-eval-harness-spec.md` (lite, depends on the citation-extraction subset of this spec for baseline metrics)
**Upstream v3.8 spec:** `docs/design/2026-05-15-issue-103-claim-alignment-audit-spec.md` (826 lines; this spec extends but does not duplicate it)

This is a **lite spec**. It documents only the v3.10 promote-and-extend delta on top of the existing v3.7.3 / v3.8 / v3.9 citation verification surface. The umbrella issue gap analysis confirmed that ~75% of #182's original "Proposal" already shipped between v3.7.3 (anchor markers + claim-faithfulness audit) and v3.9.0 (cross-index triangulation); v3.10 #182 unifies, promotes, and extends rather than rebuilds.

---

## 0. v3.11 Amendment (2026-06-02) — read this first

**Status of #182 since this spec was written.** This spec was authored 2026-05-21 under the v3.10 umbrella (#198). The umbrella shipped its other two children — #183 (epistemic-status gate) and #184 (eval harness + ranking-lift gate) — in v3.10.0 and both are CLOSED. **#182 itself was specced but NOT implemented in v3.10**: none of the §3 deliverables (`arxiv_client.py`, `verification_cache.py`, `citation_verification_summary.{schema.json,py}`, the `verification_gate/` package, `verify_passport.py`, their tests) exist on `main`, and the v3.10-delta content described in §2 never landed in the four pre-existing files it touches. Two shipped surfaces carry forward-references waiting on this work: `scripts/run_evals.py` ("`verification_gate.verify_citation` has not shipped yet (#182 Delta 5)") and `evals/gold/citation_extraction/README.md` (the Delta 4 reducer reconciles "when it does"). #182 is now scoped into **v3.11** as the one self-assessed P0 (#299). The §1–§8 body below stays valid as the implementation plan; this section amends the Delta 3 gate model only.

**What changed: Delta 3 / formatter Rule 12 gate model.** As originally written, Delta 3 promotes `ARS_CLAIM_AUDIT=1` to a *blocking default* and Rule 12 is an *unconditional* formatter REFUSE on `lookup_verified == false`. That predates the v3.10 policy-layer rescope (`docs/design/2026-05-31-ars-v3.10-policy-layer-rescope-spec.md`), which shipped the repo's canonical deterministic hard-block model: a passport-level `terminal_policies` block, **default advisory, opt-in `strict`**, where `strict` co-emits a terminal `TERMINAL-BLOCK severity=HIGH-BLOCK` token (not ack-able) alongside the advisory suffix, with the finalizer as sole policy evaluator and manual entries structurally exempt. A new gate must **inherit that model**, not introduce a second, inconsistent hard-block philosophy (the "blocking default" + the would-be per-hit override are both out of step with the shipped opt-in discipline).

The amended design (reconciled through independent cross-model design review, 2026-06-02):

1. **New `terminal_policies` key `citation_existence`** — closed enum `{advisory, strict}`, added to `shared/contracts/passport/terminal_policies.schema.json` alongside `contamination_triangulation`. **Per-key absence = advisory**, matching the existing evaluator convention (not a JSON-Schema `default`). This replaces Delta 3's `ARS_CLAIM_AUDIT` default-flip as the gate's on/off control. The v3.8 audit pipeline's *detection* still runs unconditionally (it always populates the `citation_verification_summary[]` so the signal is visible); `citation_existence` governs only whether `lookup_verified == false` is *terminal* or *advisory*.

2. **`advisory` (default)** — `lookup_verified == false` surfaces as a visible, auditable advisory: the ref **marker stays byte-equivalent to v3.9.x** (no `citation_existence` advisory suffix — the marker advisory slot is reserved for the contamination `CONTAMINATED-*` suffix and the v3.7.3 grammar caps the marker at one advisory token), and the visibility is carried by the always-populated `citation_verification_summary[]` aggregate (the `resolver_outcomes` make the criterion fully inspectable) **plus the formatter's mandatory `provenance_summary.md` `Citation Existence Advisories` section**, which lists every advisory `false` row so the warning travels with the human-reviewed deliverable. It is `/ars-mark-read` ack-able, consistent with all other advisory tiers. This default avoids false negatives on the cases that motivated `strict_articles_only` (resolver outage, metadata mismatch, non-English / regional / DOI-less coverage gaps) becoming hard stops.

3. **`strict` (opt-in)** — promotes `lookup_verified == false` to the existing terminal token: `TERMINAL-BLOCK severity=HIGH-BLOCK policy=citation_existence reason=lookup_verified_false mode=strict policy_hash=<h>`, **appended additively to the base-status token** (it does not replace the base status). The "why" survives in `reason=lookup_verified_false` + the `citation_verification_summary[]` aggregate — NOT in a marker advisory suffix: unlike `contamination_triangulation`, `citation_existence` adds no `CONTAMINATED-*`-style advisory token in either mode (C-V6(b)/(c)/(e); the marker's single advisory slot stays reserved for contamination). Terminal: **not** `/ars-mark-read` ack-able, exactly as `contamination_triangulation=strict`. The human decision lives at the **opt-in layer** (do I run this corpus under `citation_existence=strict`?), not at a per-hit override — same as the shipped precedent. This satisfies "the human decides at every gate" without inventing a new override surface.

4. **Two coupled changes, after an independent design review (2026-06-02) rejected the intermediate "data-invariant decoupling" form for false-blocking legitimate unindexed citations and for a "Zombie pipeline" UX contradiction (advisory-but-ack-able yet unconditionally blocking).** The root cause was not the gate shape but the over-broad `false` definition. Both are fixed:

   **(4a) Narrow the `lookup_verified == false` definition by query form (Delta 4 refinement).** The shipped Delta 4 classifies `false` as "≥1 resolver `unmatched` AND no `matched`" without distinguishing *how* the resolver was queried. That conflates two very different signals:
   - **ID-keyed unmatched = positive non-existence evidence.** A resolver queried by an exact key (DOI or arXiv ID) that returns `unmatched` is strong fabrication evidence: the ID is a precise lookup key, so "this DOI does not resolve" means the cited identifier is bogus.
   - **Title-only unmatched = coverage gap, NOT fabrication.** A resolver queried only by author/year/title fragment that returns `unmatched` is *weak* evidence: a real regional / non-English / pre-digital paper indexed nowhere produces exactly this, and is a legitimate coverage gap — the same case `strict_articles_only` was designed to leave advisory.

   So `lookup_verified == false` is **narrowed** to require **at least one ID-keyed `unmatched`** (a DOI or arXiv ID that provably fails to resolve) with no `matched`. A citation whose only negative signals are title-only `unmatched` (no DOI/arXiv ID was present to key on, or the ID-keyed lookups were `unreachable`/`skipped`) resolves to **`unresolvable`**, not `false` — it carries the advisory triangulation suffix (visible coverage-gap signal) but is never a `false`. This aligns the existence gate's `false` with the `strict_articles_only` precision philosophy (fabrication requires a falsifiable identifier, not mere absence-from-index).

   **(4b) `ready-for-review` gate is a true opt-in, symmetric with `contamination_triangulation`.** With `false` now meaning "provably-bogus identifier," the gate returns to the shipped opt-in symmetry: `ready-for-review` is blocked by `lookup_verified == false` **only under `citation_existence == strict`**, exactly as `contamination_triangulation == strict` gates its own terminal block. Under default `advisory`, a `false` row is a `/ars-mark-read`-ack-able advisory and the pipeline completes — no Zombie trap, no back-compat break (a key-absent v3.10 passport behaves identically to v3.9.x). The P0 intent (`a draft cannot reach "ready for human review" status if any citation has lookup_verified: false`) is honored **for users who opt into `strict`**, which — given the narrowed `false` — now means "I want provably-bogus DOIs/arXiv IDs to hard-block," a precise and safe default to opt into. The earlier "data invariant independent of policy" form (4-prev) is **withdrawn**: it false-blocked legitimate unindexed citations because it inherited the over-broad `false`.

**Why this is not the Co-Scientist L1 red line.** L1 forbids *hidden* ranking/culling: AI scores → AI selects top-K → user sees only survivors, with the filter criterion unauditable. Rule 12 is the opposite on every axis: the judgment is external-API factual (paper exists or not), not AI subjective scoring; the blocked/flagged citation stays **visible and explicitly annotated**; and `resolver_outcomes` makes the criterion **fully auditable**. Nothing is silently narrowed. The relevant boundary is **L4 question 3 (who may ROUTE to the next stage)**: the finalizer — already the *sole policy evaluator* under the shipped model — performs both the marker stamping and the `ready-for-review` data-invariant check, adding no new control-plane writer. The user-confirmation invariant is preserved on two independent axes: the marker severity is the user's opt-in (`citation_existence` advisory/strict), and the `ready-for-review` certification is a deterministic, auditable data check — not an AI routing decision — that the user still confirms at the checkpoint.

See INVARIANT **C-V6** (§5) for the binding form of this amendment. The §6 acceptance criteria B4 and B8 are re-pointed accordingly: B4's "default flip" reads as "the `citation_existence` key governs marker severity (absence = advisory) and gates `ready-for-review` only under `strict` (C-V6(d)), with `false` narrowed to ID-keyed unmatched (C-V6(a))"; B8's "Reviewer column separate from blocking" is satisfied, under `strict`, by the terminal block and the strict-only `ready-for-review` gate — not by an unconditional Rule 12.

---

## 1. What already ships (do not rebuild)

The following surface lands before v3.10 and is consumed by this spec without modification. Read the cited v3.8 spec for the contract details; this lite spec assumes familiarity.

| Capability | Existing path | First shipped |
|---|---|---|
| Crossref DOI lookup + title fallback + backoff | `scripts/crossref_client.py` | v3.8 |
| OpenAlex production client | `scripts/openalex_client.py` | v3.9.0 |
| Parallel Semantic Scholar / OpenAlex / Crossref triangulation | `deep-research/agents/bibliography_agent.md` (Triangulation Extension) | v3.9.0 |
| Three-layer source-text anchor markers `<!--anchor:<kind>:<value>-->` | (in writer agent prompts) | v3.7.3 |
| Claim-faithfulness audit pipeline + 8-row finalizer matrix + 5 HIGH-WARN refusal classes | `scripts/claim_audit_pipeline.py`, `scripts/claim_audit_finalizer.py`, `academic-paper/agents/formatter_agent.md` | v3.8 |
| First-party Crossref `issued` + pdftotext cover-page provenance | `shared/contracts/passport/citation_provenance.schema.json` | v3.9.4 |
| Contamination signals (preprint / S2-unmatched / OpenAlex-unmatched / Crossref-unmatched) | `shared/contracts/passport/literature_corpus_entry.schema.json` (`contamination_signals` object) | v3.7.3 (S2), v3.9.0 (OpenAlex + Crossref) |
| `ARS_CLAIM_AUDIT=1` opt-in flag | `academic-pipeline/SKILL.md` (v3.8 mode flag rationale, §5 of v3.8 spec) | v3.8 (default OFF) |

#182's original proposal asked for "a new module performs Crossref/DOI/arXiv resolution with caching." Everything except arXiv and caching is already shipped; the unified blocking gate is also already shipped at Stage 4→5 but gated behind `ARS_CLAIM_AUDIT=1`. v3.10 ships five delta items below.

## 2. Five v3.10 delta items

### Delta 1 — arXiv API resolver

The current arXiv surface is a preprint-risk heuristic at `scripts/contamination_signals.py` plus venue-string matching in `bibliography_agent`. There is no API call to verify a cited arXiv ID resolves to a real preprint with matching metadata. v3.10 adds:

- **`scripts/arxiv_client.py`** — new module mirroring the `crossref_client.py` shape. Implements DOI-lookup-equivalent for arXiv IDs (`arxiv_id_lookup`), title-fallback search, exponential backoff, and the same unmatched semantics (`resolve_arxiv_unmatched`) used by the v3.9.0 triangulation pipeline.
- **`scripts/contamination_signals.py` signal computation**: add `arxiv_unmatched` boolean computation to the per-citation signal pass (mirrors how `crossref_unmatched` + `openalex_unmatched` are computed today). This file scope is **signal computation only**; the matrix dispatch and formatter pass-through changes live in the surfaces below.
- **`shared/contracts/passport/literature_corpus_entry.schema.json` schema extension**: add optional `arxiv_unmatched` boolean field to `contamination_signals` alongside the v3.9.0 `crossref_unmatched` + `openalex_unmatched` triplet. The manual-entry not-rule extends from v3.9.0 `anyOf: [s2, openalex, crossref]` to include `arxiv`.
- **`academic-pipeline/agents/pipeline_orchestrator_agent.md` §3.6 finalizer matrix advisory tier extension**: v3.9.0's `k=0..3` advisory matrix extends to `k=0..4` to cover four-index triangulation outcomes. The annotation token vocabulary extends with `CONTAMINATED-ARXIV-UNMATCHED` (single-index k=1 on arxiv only) and `CONTAMINATED-QUADRANGULATION-UNMATCHED` (full four-index k=4), and the v3.7.3 / v3.9.0 PREPRINT composition rule extends to compose these new tokens too: `CONTAMINATED-PREPRINT+ARXIV-UNMATCHED` and `CONTAMINATED-PREPRINT+QUADRANGULATION-UNMATCHED` (PREPRINT prefix first per canonical token order). The terminal gate behavior remains unchanged from v3.7.3 / v3.9.0 (advisory only; no new gate-refuse classes from triangulation).
- **`scripts/claim_audit_finalizer.py` dispatch**: implement the matrix extension defined by the orchestrator above. The 8-row finalizer matrix is **not** extended (no new rows); the suffix tokens compose at the annotation-rendering layer per the v3.9.0 advisory-suffix pattern.
- **`academic-paper/agents/formatter_agent.md` pass-through allowlist**: extends from 9 v3.9.0 tokens to 13 v3.10 tokens (adds the 4 new tokens above: `CONTAMINATED-ARXIV-UNMATCHED`, `CONTAMINATED-QUADRANGULATION-UNMATCHED`, `CONTAMINATED-PREPRINT+ARXIV-UNMATCHED`, `CONTAMINATED-PREPRINT+QUADRANGULATION-UNMATCHED`). Refusal rules 1-10 unchanged.

Out of scope for Delta 1: arXiv full-text resolution. The arXiv API verifies metadata and existence; it does not feed the v3.10 #183 epistemic-status extractor's Tier 2 pdftotext pathway. A user who wants Tier 2 verification on an arXiv preprint must still provide a local PDF via `source_pointer`. This separation keeps the citation gate (this spec) orthogonal to the epistemic-status gate (#183).

### Delta 2 — Persistent verification cache

Crossref / OpenAlex / S2 clients today implement per-request backoff but no persistent caching across pipeline runs. The same paper cited in multiple drafts re-hits the API every time. v3.10 adds:

- **`scripts/verification_cache.py`** — new module managing a local SQLite-backed cache (file at `~/.cache/ars/verification.db`, configurable via `ARS_VERIFICATION_CACHE_PATH`). Cache key is `(citation_key, resolver_name, query_form)` where `resolver_name ∈ {crossref, openalex, semantic_scholar, arxiv}` and `query_form` is canonical-form of the DOI / arXiv ID / title-query string. Cache value is the resolver's structured response plus a verification timestamp.
- **Cache invalidation policy**: TTL 90 days from verification timestamp; manual invalidation via `ars-cache-invalidate <citation_key>` slash command (added under deliverable 6 below). The 90-day window is a guess; OQ-1 lists it as deferred for empirical tuning.
- **Cache hit semantics**: a cache hit returns the structured response immediately and skips the API call. Cache miss or expired entry triggers the live API call; on success, the cache is populated. The four resolvers (crossref, openalex, semantic_scholar, arxiv) share the cache schema but key their entries by `resolver_name`.
- **Concurrency**: SQLite WAL mode for single-writer-many-readers safety. The audit pipeline is single-process; concurrent runs (multi-user shared cache) are out of scope for v3.10.

Cache integration touches the four existing resolver modules (crossref_client.py, openalex_client.py, the S2 lookup in `bibliography_agent`, the new arxiv_client.py) with a uniform wrapper: each resolver's public entry point grows a `cache: VerificationCache | None = None` parameter and, when present, consults the cache before making the network call. v3.8 calibration runner and audit pipeline tests pass a `cache=None` to preserve byte-equivalent behavior on test inputs.

### Delta 3 — Promote `ARS_CLAIM_AUDIT=1` from opt-in to blocking default

> **⚠ Superseded by §0 / C-V6 (v3.11 amendment).** The "blocking default" gate model below is the v3.10 original design and is retained for rationale. The gate's on/off control is now the `terminal_policies.citation_existence` key (default advisory, opt-in strict), NOT an `ARS_CLAIM_AUDIT` default flip, and Rule 12 is the `strict`-only terminal block, NOT an unconditional REFUSE. Read §0 and C-V6 for the binding form; the calibration-evidence and load-bearing-safety reasoning in this Delta still applies to why the existence signal is detected unconditionally.

v3.8 ships `ARS_CLAIM_AUDIT=1` as default OFF per v3.8 spec §5 mode flag rationale ("ramp-on plan deferred to post-calibration evidence"). v3.9.x runs accumulated the empirical evidence the v3.8 rationale required (calibration runner at `scripts/claim_audit_calibration.py`, T-C1/T-C2/T-C3 thresholds passing on the 20-tuple gold set per `academic-pipeline/references/claim_audit_calibration_protocol.md`). v3.10 promotes the gate.

The promotion is **policy-only** (no code-path change beyond the flag default flip and one branch deletion):

- **`academic-pipeline/SKILL.md` flag rationale section**: the v3.8 paragraph at line 25 ("v3.8 (opt-in): Set `ARS_CLAIM_AUDIT=1` ...") is replaced with a v3.10 paragraph stating the gate is on by default and the flag now exists only for opt-out backwards compatibility (`ARS_CLAIM_AUDIT=0` skips the gate; default behavior dispatches the audit regardless of the env-var presence).
- **`academic-pipeline/agents/pipeline_orchestrator_agent.md` §3.6 dispatch**: the conditional dispatch (`if env('ARS_CLAIM_AUDIT') == '1'`) becomes unconditional dispatch; the explicit opt-out branch (`if env('ARS_CLAIM_AUDIT') == '0': skip`) is added.
- **`scripts/test_claim_audit_pipeline.py`** updates: the existing flag-on test cases become the default, the flag-off test cases become the opt-out path. T-C1 / T-C2 / T-C3 calibration thresholds and the 20-tuple gold set are unchanged.

The promotion's load-bearing safety property: the formatter HIGH-WARN refusal classes (rules 6-10 per v3.8 spec) now block production output by default for the five classes the v3.8 audit already detected (HIGH-WARN-CLAIM-NOT-SUPPORTED, HIGH-WARN-NEGATIVE-CONSTRAINT-VIOLATION, HIGH-WARN-FABRICATED-REFERENCE, HIGH-WARN-CLAIM-AUDIT-ANCHORLESS, HIGH-WARN-CONSTRAINT-VIOLATION-UNCITED). Per #198 acceptance criterion, this delta is what makes the v3.10 verification layer the **default critical path** rather than an opt-in advisory pass.

**Formatter REFUSE list extension (v3.10 rule 12):** Delta 3 adds a new formatter refusal class to `academic-paper/agents/formatter_agent.md` `Cite-Time Provenance Hard Gate`, sibling to rules 6-10 (v3.8) and rule 11 (v3.10 silent-upgrade per #183 §6.1):

- **Rule 12 (NEW v3.10 — amended by C-V6):** ~~Refuse on any~~ Under `terminal_policies.citation_existence == strict`, refuse on any `citation_verification_summary[]` row where `lookup_verified == false` — where `false` is the **narrowed** C-V6(a) form (≥1 ID-keyed DOI/arXiv `unmatched`, no `matched`); a title-only-unmatched real-but-unindexed citation is `unresolvable`, never refused. Refusal annotation: `[UNVERIFIED CITATION — lookup_verified=false: citation_key {citation_key}, ref_slug {ref_slug}, resolvers_unmatched=[{list of resolver names that returned status=unmatched}]]`, emitted by the formatter on refusal alongside the terminal token's `reason=lookup_verified_false` and the `citation_verification_summary[]` aggregate (NOT a marker advisory suffix — `citation_existence` adds none, C-V6(b)/(c)/(e)). Rule 12 has no dependency on reviewer agent verdict; it is a deterministic gate-refuse triggered by the unified summary produced by Delta 4. **(v3.11 amendment):** under default `advisory`, the same row produces the annotation as a `/ars-mark-read`-ack-able advisory, NOT a refusal — the `strict` opt-in is what makes it terminal. See C-V6.

This refusal class is what satisfies acceptance criterion B8 below. The rule is cross-referenced into the #183 formatter refusal list extension table at §6.1 so the two specs share a single source of truth for the v3.10 refusal surface. The `unresolvable` outcome does NOT trigger Rule 12 per OQ-4's advisory-pass default; only `false` does.

Backwards-compatibility note: v3.8 / v3.9.x corpora that contain unreviewed audit findings stored in the passport pre-v3.10 are not retroactively re-audited. v3.10 applies only to new pipeline runs.

### Delta 4 — Unified `lookup_verified` + `anchor_present` per-citation status surface

Today the citation-level verification signals are split across three schemas: anchor presence lives in v3.7.3 anchor markers (read at `formatter_agent`'s `Cite-Time Provenance Hard Gate`), Crossref/OpenAlex/S2 unmatched signals live in `contamination_signals`, and claim-audit verdicts live in `claim_audit_result`. A consumer that wants to know "is this citation verified or not" must read three schemas and apply join logic. v3.10 adds a derived view:

- **`shared/contracts/passport/citation_verification_summary.schema.json`** — new aggregate schema. One entry per (citation_key, ref_slug) pair. Required fields:
  - `citation_key`: joins to `literature_corpus_entry.citation_key`.
  - `ref_slug`: joins to the `<!--ref:slug-->` markers used in writer prose.
  - `lookup_verified`: enum of `{true, false, unresolvable}`. `true` iff at least one applicable resolver returned `status=matched`. `false` iff at least one applicable resolver returned `status=unmatched` AND no applicable resolver returned `status=matched` (i.e., positive evidence of non-existence from at least one source). `unresolvable` iff **every** applicable resolver returned `status=unreachable` AND none returned (`status=matched` OR `status=unmatched`) (i.e., the system could not adjudicate because every applicable resolver was offline). The classification is anti-fabrication-biased: a fabricated citation with bogus DOI that produces three `unmatched` + one `unreachable` is classified `false`, not `unresolvable`, because a single `unmatched` is positive evidence of non-existence and one transient outage does not cancel that evidence. `skipped` outcomes are excluded from the classification condition entirely; they reflect resolver applicability or policy (arXiv resolver `skipped` on non-arXiv citation per Delta 1; manual entry `skipped` per v3.7.3 `obtained_via: manual` exemption), not adjudication. When all applicable resolvers are `skipped` (e.g., a manual entry that no resolver attempts), the set of adjudicating resolvers is empty and `lookup_verified` defaults to `unresolvable`; this is the manual-entry-exempt case and matches the v3.9.x triangulation advisory tier. Downstream consumers needing to distinguish manual exemption from total outage read `resolver_outcomes.*.status` directly.
  - `anchor_present`: boolean. `true` iff the citation's v3.7.3 anchor marker has `kind ∈ {quote, page, section, paragraph}` (not `none`).
  - `verification_timestamp`: ISO-8601 UTC. When at least one resolver ran, this is the latest resolver timestamp. When all resolvers were skipped (manual entry) or all unreachable (outage), this is the attempt timestamp (when the summary was generated). Never null.
  - `resolver_outcomes`: object with one optional field per resolver (`crossref`, `openalex`, `semantic_scholar`, `arxiv`); each value is `{status: "matched" | "unmatched" | "unreachable" | "skipped", response_summary: string | null}`. The `status` enum is one-of (mutually exclusive states); replaces the three-boolean encoding from an earlier draft of this spec which permitted contradictory combinations. `matched` = resolver returned a match. `unmatched` = resolver ran and returned no match. `unreachable` = API outage / timeout. `skipped` = resolver did not run (manual entry per v3.7.3 exemption, or arXiv resolver on a non-arXiv citation per Delta 1).
- **Derivation point**: `scripts/citation_verification_summary.py` (new module) generates this aggregate at the Stage 4→5 boundary by joining the existing three sources. It runs after the v3.8 audit pipeline and before the formatter REFUSE pass.

This summary is the read interface for the §3.6 `verification_gate` contract (per #183 spec). The other two children of #198 reference this surface:

- **#183 epistemic-status gate** reads `anchor_present` to know whether the row is Shape C.1 (anchorless skip per #183 §5.7.2) or eligible for the v3.10 emitted-status inference.
- **#184 eval harness** reads `lookup_verified` as the per-citation truth signal for the citation-extraction subset of its gold set.

`lookup_verified` semantics intentionally omit a "partial match" or "conflict" value. v3.9.0's triangulation already encodes those nuances in `contamination_signals.k_unmatched`; the v3.10 summary collapses them to a binary "did at least one external index agree." A consumer that needs the fine-grained breakdown reads `resolver_outcomes` directly.

### Delta 5 — `verification_gate` standalone API surface

#198's acceptance criterion 4 requires "each child gate emits output in the shared shape so downstream consumers can read all three uniformly." Delivering this on the citation side requires extracting the gate logic from its current Stage 4→5 finalizer-step encoding into a callable API.

- **`scripts/verification_gate/__init__.py`** — new package wrapping the citation verification logic. Public functions:
  - `verify_citation(citation_key, manifest, corpus, cache=None) -> CitationVerificationOutcome` — runs the full triangulation (Crossref + OpenAlex + S2 + optional arXiv) and returns a structured outcome (`lookup_verified`, `anchor_present`, `resolver_outcomes`, `verification_timestamp`).
  - `verify_passport(passport, cache=None) -> list[CitationVerificationOutcome]` — batch helper running `verify_citation` over every (citation_key, ref_slug) pair in the passport.
- The new API does **not** duplicate the v3.8 audit pipeline; it composes the existing crossref_client / openalex_client / S2 lookup / new arxiv_client (Delta 1) and writes the unified summary schema (Delta 4). The v3.8 audit pipeline's audit-finding writers (which produce `claim_audit_result` rows) continue to call into the same underlying resolvers; the new API and the v3.8 audit are two callers of the same lower-layer infrastructure rather than duplicate implementations.
- The API is invoked at three points in v3.10:
  1. Stage 4→5 audit pipeline (existing, now unconditional per Delta 3).
  2. Standalone command-line entry point for ad-hoc verification (`scripts/verify_passport.py`, callable as `python -m scripts.verify_passport <passport.yaml>`).
  3. Future consumers — the #184 eval harness reads via this API during gold-set comparison.

The API stability contract: function signatures and return shapes are frozen at v3.10.0 and only additively extended in patch releases. Removing a field from `CitationVerificationOutcome` requires a minor-version bump and a deprecation cycle.

## 3. Deliverable list (paths)

1. `scripts/arxiv_client.py` (Delta 1)
2. `shared/contracts/passport/literature_corpus_entry.schema.json` — `contamination_signals.arxiv_unmatched` field addition (Delta 1)
3. `scripts/contamination_signals.py` — arxiv triangulation extension to k=0..4 advisory matrix (Delta 1)
4. `scripts/verification_cache.py` (Delta 2)
5. `scripts/crossref_client.py`, `scripts/openalex_client.py`, the Semantic Scholar lookup helper invoked from `deep-research/agents/bibliography_agent.md` (implementation lives in the bibliography pipeline; needs verification at implementation time for exact path/function name), and the new `scripts/arxiv_client.py` (Delta 1) — `cache: VerificationCache | None = None` parameter addition on each resolver entry point (Delta 2)
6. `commands/ars-cache-invalidate.md` — new `/ars-cache-invalidate <citation_key>` slash command (Delta 2)
7. **(amended, C-V6)** `shared/contracts/passport/terminal_policies.schema.json` — add `citation_existence` closed-enum `{advisory, strict}` key (default advisory via the existing per-key-absence evaluator convention). Plus `academic-pipeline/SKILL.md` — document the `citation_existence` policy (marker severity + the `strict`-only `ready-for-review` gate, symmetric with `contamination_triangulation` per C-V6(d)) and the narrowed `false` definition (ID-keyed unmatched only, C-V6(a)). (Supersedes the original Delta-3 `ARS_CLAIM_AUDIT` flag-flip deliverable.)
8. **(amended, C-V6)** `academic-pipeline/agents/pipeline_orchestrator_agent.md` — finalizer (sole evaluator) does two recomputed-each-pass things (C-V6(h)): (i) under `citation_existence == strict`, co-emit `TERMINAL-BLOCK ... policy=citation_existence reason=lookup_verified_false` per the shipped grammar, additively with any contamination terminal token (C-V6(g)); (ii) gate `ready-for-review` on `lookup_verified == false` ONLY under `citation_existence == strict` (C-V6(d), symmetric with contamination). (Supersedes the original Delta-3 §3.6 dispatch-flip deliverable.)
9. `shared/contracts/passport/citation_verification_summary.schema.json` (Delta 4)
10. `scripts/citation_verification_summary.py` (Delta 4)
11. `scripts/verification_gate/__init__.py` (Delta 5)
12. `scripts/verify_passport.py` — standalone CLI entry point (Delta 5)
13. **Tests** — `scripts/test_arxiv_client.py`, `scripts/test_verification_cache.py`, `scripts/test_citation_verification_summary.py`, `scripts/test_verification_gate.py`, and **(amended, C-V6)** `scripts/test_citation_existence_policy.py` covering: (a) **narrowed `false`** — an ID-keyed (DOI/arXiv) `unmatched` yields `false`; a title-only `unmatched` with no resolvable ID yields `unresolvable`, NOT `false` (the unindexed-real-paper case); (b) default-advisory does NOT block flow and renders the ack-able advisory; (c) opt-in-strict co-emits the terminal `HIGH-BLOCK` token; (d) `ready-for-review` is blocked by `false` ONLY under `strict` and completes under advisory (true opt-in, symmetric with contamination); (f) manual-exempt + `unresolvable` (incl. title-only) rows never block; (g) a ref hit by both `contamination_triangulation=strict` and `citation_existence=strict` carries two terminal tokens but dedupes to one affected ref; (h) flipping advisory→strict re-applies the gate on the next finalize and a resumed passport recomputes (no stale `ready-for-review` survives a `false` under strict). Renamed from the original `test_default_blocking_gate.py`. **Mutation checks** per repo discipline: a trivial that treats title-only `unmatched` as `false` MUST fail case (a); a trivial accept-all that ignores `citation_existence` MUST fail case (d).
14. CI wiring: extend `.github/workflows/spec-consistency.yml` path filter to trigger on `scripts/arxiv_client.py`, `scripts/verification_cache.py`, `scripts/verification_gate/**`, the two new schemas, the `terminal_policies.schema.json` change, and the new test paths. The existing `scripts/check_v3_10_policy.py` lint (which already parses `terminal_policies`) gains `citation_existence` enum + grammar coverage.
15. `CHANGELOG.md` + `docs/ARCHITECTURE.md` + ROADMAP §3.10 anchor — v3.10 release-alignment 6-invariant set per repo discipline.

## 4. Out of scope

- **Replacing the v3.8 audit pipeline.** v3.10 #182 adds a parallel verification API surface; the v3.8 `claim_audit_pipeline.py` continues to run with its existing 8-row finalizer dispatch. v3.8 rows and the new `citation_verification_summary[]` aggregate are independent passport surfaces; downstream consumers read whichever fits their use case.
- **Anchor-aware citation insertion during draft generation.** Already shipped in v3.7.3 three-layer emission; not a v3.10 deliverable.
- **Multi-user shared cache.** Concurrent runs sharing a single cache file are out of scope. The cache assumes single-process access; multi-process is a future follow-up if usage demands.
- **Conflict resolution between resolvers.** When Crossref says match and OpenAlex says unmatched, v3.10 reports both via `resolver_outcomes` and `lookup_verified` is `true` (at-least-one match). A "Crossref disagrees with OpenAlex on the matched paper's metadata" reconciliation is out of scope; v3.9.4 `citation_provenance` already covers the year-precision disagreement case for first-party verification, which is the only conflict type the existing surface handles.
- **`evidence_type` 6-tier biomedical refinement.** Out of scope per #198 umbrella (the v3.10 vocabulary work belongs to #183 and is locked to the 5-tier cross-disciplinary enum).

## 5. INVARIANTS

I-V1 through I-V15 from `2026-05-21-v3.10-183-epistemic-status-spec.md` §9 are not duplicated here. The following are #182-specific:

**C-V1 — Triangulation cardinality.** `contamination_signals.k_unmatched` reports the count of resolvers that returned unmatched among those that ran. With arXiv added, `k` ranges from 0 (all matched) to 4 (all four unmatched on a citation that has an arXiv ID); for citations without an arXiv ID, the upper bound is 3. The `arxiv_unmatched` field is optional in the schema and absent when the citation has no arXiv ID; lint asserts this absence-vs-`false` distinction.

**C-V2 — Cache transparency.** Cache hits and misses produce byte-equivalent outputs at the v3.10 API level. A consumer cannot distinguish a cache hit from a fresh API call by inspecting the returned `CitationVerificationOutcome` (other than via `verification_timestamp`, which carries the original verification time on cache hits).

**C-V3 — Flag back-compat (scoped).** The opt-out path (`ARS_CLAIM_AUDIT=0`) MUST suppress all v3.10-added passport aggregates introduced by this spec (`citation_verification_summary[]`, `verification_gate` standalone API output, the new `arxiv_unmatched` contamination signal field). When suppressed, the passport surface on existing v3.8 / v3.9.x aggregates (`claim_audit_results[]`, `contamination_signals` minus `arxiv_unmatched`, etc.) MUST be byte-equivalent to a v3.9.x run on the same input. The byte-equivalence guarantee is scoped to the existing aggregates, not to the passport as a whole; the v3.10-added aggregates are present-or-absent depending on the flag, not "byte-equivalent to v3.9.x absent state" (a v3.9.x run had no concept of these aggregates, so byte-equivalence is undefined).

**C-V4 — verification_gate API stability.** The `verify_citation` and `verify_passport` signatures are frozen at v3.10.0. Patch releases add optional keyword arguments only; minor releases may add new required fields with a deprecation cycle and migration script.

**C-V5 — Anchor-present semantics.** `anchor_present == true` is the strict complement of v3.7.3's anchor-none case. `anchor_kind ∈ {quote, page, section, paragraph}` maps to `true`; `anchor_kind == "none"` maps to `false`. No third value is permitted; if an unrecognized `anchor_kind` appears, the row is a v3.7.3 contract violation and the v3.8 audit pipeline rejects it before this summary runs.

**C-V6 — Citation-existence gate inherits `terminal_policies` with a narrowed `false` definition (v3.11 amendment, supersedes Delta 3's blocking-default form).** The `lookup_verified == false` gate is governed by the `terminal_policies.citation_existence` key (enum `{advisory, strict}`), NOT by an `ARS_CLAIM_AUDIT` default flip and NOT by an unconditional formatter REFUSE. Binding sub-clauses:
- **(a) `false` requires an ID-keyed unmatched (Delta 4 refinement, §0(4a)).** `lookup_verified == false` requires **at least one resolver queried by an exact identifier (DOI or arXiv ID) returning `unmatched`**, with no `matched`. Title-only `unmatched` (no resolvable DOI/arXiv ID was available to key on) does NOT yield `false`; it yields `unresolvable` and carries the advisory triangulation suffix. This narrows the shipped Delta 4 line-112 definition, which classified any `unmatched`-without-`matched` as `false` and would false-block legitimate unindexed (regional / non-English / pre-digital) citations. Rationale: a falsifiable identifier that provably fails to resolve is fabrication evidence; absence-from-index without a falsifiable identifier is a coverage gap. Mirrors the `strict_articles_only` precision philosophy.
- **(b) Default advisory.** Per-key absence ⟹ advisory (evaluator convention, not a JSON-Schema `default`); a whole-object-absent passport ⟹ advisory for this key (so a v3.10 passport behaves identically to v3.9.x — no back-compat break). Under advisory, `lookup_verified == false` is a visible, `/ars-mark-read`-ack-able advisory — never a block, and the pipeline completes normally. **The ref marker stays byte-equivalent to v3.9.x** (no `citation_existence` advisory suffix — the marker's single advisory slot is the contamination `CONTAMINATED-*` suffix; the v3.7.3 grammar caps the marker at one advisory token, so a second would break it). The advisory's visibility is carried NOT by the marker but by the always-populated `citation_verification_summary[]` aggregate **and the formatter's mandatory `provenance_summary.md` `Citation Existence Advisories` section** (see `academic-paper/agents/formatter_agent.md`), which lists every advisory `false` row (ref_slug, citation_key, the falsifiable DOI/arXiv ID, resolvers_unmatched) so the warning travels with the human-reviewed deliverable. This resolves the apparent tension between "byte-equivalent to v3.9.x" and visibility: the marker bytes do not change, and the visibility lives in the output package, not the marker.
- **(c) Opt-in strict is terminal and not ack-able.** Under `citation_existence == strict`, the finalizer (the sole policy evaluator — no new control-plane writer is introduced, satisfying L4 question 3) appends `TERMINAL-BLOCK severity=HIGH-BLOCK policy=citation_existence reason=lookup_verified_false mode=strict policy_hash=<h>` to the ref marker. This is **additive** to the base-status token, mirroring the *terminal-token mechanism* of `contamination_triangulation=strict` — but UNLIKE contamination, `citation_existence` adds **no** advisory suffix to the marker's advisory slot (per C-V6(b) / (e), in either mode); the `false` "why" survives in the terminal token's `reason=lookup_verified_false` plus the `citation_verification_summary[]` aggregate (and, default-mode, the `provenance_summary.md` advisory section), NOT in a marker advisory suffix. The block is terminal: NOT `/ars-mark-read`-ack-able. There is NO per-hit override token; the human decision is the opt-in itself.
- **(d) `ready-for-review` is gated symmetrically with contamination — a true opt-in, NOT a policy-independent data invariant.** Status promotion to `ready-for-review` is blocked by `lookup_verified == false` **only under `citation_existence == strict`**, exactly as `contamination_triangulation == strict` gates its own terminal block. Under default advisory the run completes (the `false` row is an ack-able advisory). This avoids the "Zombie pipeline" contradiction (an advisory-yet-unconditionally-blocking state) that the withdrawn policy-independent-invariant form created. The P0 intent (`a draft cannot reach "ready for human review" status if any citation has lookup_verified: false`) is honored for users who opt into `strict`; given the narrowed (a) `false`, opting in means "provably-bogus DOIs/arXiv IDs hard-block," a precise and safe choice. Both the prior "requires strict" form and the prior "data invariant independent of policy" form are **withdrawn**.
- **(e) Detection is unconditional; only terminality is policy-gated.** The `citation_verification_summary[]` aggregate (Delta 4) is always populated so the signal is always visible regardless of `citation_existence`; the policy governs whether a `false` row promotes to a *terminal* marker (`strict`, the `TERMINAL-BLOCK` token) or stays advisory (default). Under default advisory the **ref marker is byte-equivalent to v3.9.x** — there is NO `citation_existence` marker suffix in either mode (the marker's single advisory slot is the contamination `CONTAMINATED-*` suffix, per C-V6(b)); the advisory's visibility is carried by the aggregate + the formatter's mandatory `provenance_summary.md` `Citation Existence Advisories` section, NOT by a marker annotation. C-V6(b) supersedes the original C-V3 `ARS_CLAIM_AUDIT=0` suppression for the existence gate specifically — the existence signal is never suppressed, only de-escalated to advisory.
- **(f) Manual-entry and unindexed exemptions preserved.** A manual entry whose applicable resolvers are all `skipped` yields `unresolvable` (not `false`); a legitimately-unindexed citation with only title-only `unmatched` yields `unresolvable` per (a). Neither triggers the `strict` terminal block, matching the shipped `terminal_policies` manual-entry exemption and the `strict_articles_only` coverage-gap carve-out. `unresolvable` follows OQ-4's advisory default regardless of `citation_existence`.
- **(g) Multiple terminal policies co-emit independently.** A single ref that violates both `contamination_triangulation == strict` (k=3) AND `citation_existence == strict` (`lookup_verified == false`) carries **two** `TERMINAL-BLOCK` tokens in its marker, one per `policy=` value, alongside the shared advisory slot — the tokens are additive at the marker, but the ref is counted once in any "total affected refs" aggregate (dedupe by ref slug across policy buckets, mirroring the shipped contamination `terminal_blocked[]` non-additive rule). The formatter's generic "refuse on any unresolved `HIGH-BLOCK`" rule already handles N tokens without per-policy enumeration.
- **(h) Policy flip and resume re-evaluate; nothing is cached.** Both the marker severity (c) and the `ready-for-review` strict gate (d) are recomputed by the finalizer (the sole evaluator) at each finalization pass — they are functions of the current `terminal_policies` state and the current `citation_verification_summary[]`, not cached status. Flipping `citation_existence` advisory→strict between passes re-stamps markers and re-applies the strict gate on the next finalize; a `resume_from_passport` / reset that re-enters finalization re-evaluates against the resumed summary. A previously-granted `ready-for-review` is never inherited across a resume without re-passing the gate under the then-current policy — there is no path where a stale "ready" certification survives a citation that resolves to `false` under `strict`.

## 6. Acceptance criteria

**B1 — arXiv client shipped.** `scripts/arxiv_client.py` implements DOI-equivalent ID lookup, title fallback, exponential backoff, and `resolve_arxiv_unmatched`. Unit tests pass against fixture responses; integration test against the live arXiv API runs nightly (gated behind a network-available CI flag).

**B2 — Triangulation extended to four indexes.** `contamination_signals.arxiv_unmatched` field lands in `literature_corpus_entry.schema.json`. Signal computation lands in `scripts/contamination_signals.py`. The k=0..4 advisory matrix lands in `academic-pipeline/agents/pipeline_orchestrator_agent.md` §3.6 with four new annotation tokens (`CONTAMINATED-ARXIV-UNMATCHED`, `CONTAMINATED-QUADRANGULATION-UNMATCHED`, and their two PREPRINT-composed variants). Dispatch lands in `scripts/claim_audit_finalizer.py`. v3.9.0 formatter pass-through allowlist in `academic-paper/agents/formatter_agent.md` extends from 9 to 13 to cover the four new tokens.

**B3 — Persistent cache shipped.** `scripts/verification_cache.py` operational; SQLite WAL mode; default path `~/.cache/ars/verification.db`; `ARS_VERIFICATION_CACHE_PATH` env-var override functional. Four resolver modules accept the `cache` parameter and consult/populate it correctly. `scripts/test_verification_cache.py` exercises hit / miss / expired / WAL-concurrent-read cases.

**B4 — Citation-existence gate shipped (amended, C-V6 — supersedes the original "blocking-default promotion").** The `terminal_policies.citation_existence` key lands (default advisory; opt-in `strict`), with `false` narrowed to ID-keyed unmatched (C-V6(a)). A default-advisory run produces the existence annotation as an ack-able advisory and preserves v3.9.x non-blocking behavior; a `strict` run co-emits the `TERMINAL-BLOCK` token and gates `ready-for-review`, symmetric with `contamination_triangulation` (C-V6(d)). Existing v3.8 audit detection still runs unconditionally (C-V6(e)). A title-only `unmatched` real-but-unindexed citation resolves to `unresolvable` and never blocks (C-V6(a)/(f)). *(Original B4 text, for rationale: "`ARS_CLAIM_AUDIT=1` default flip lands … `ARS_CLAIM_AUDIT=0` opt-out preserves v3.9.x behavior byte-for-byte (C-V3)." — the `ARS_CLAIM_AUDIT` flag is no longer the gate control; C-V6 governs.)*

**B5 — Citation verification summary shipped.** `citation_verification_summary.schema.json` and `citation_verification_summary.py` land. The summary aggregate appears in passport runs after v3.10 audit pipeline completes. Downstream readers (#183 epistemic-status gate, #184 eval harness) can consume the summary by reading the schema.

**B6 — verification_gate API shipped.** `scripts/verification_gate/__init__.py` exposes `verify_citation` and `verify_passport`. `scripts/verify_passport.py` callable as `python -m scripts.verify_passport <passport>`. API stability contract documented in module docstring.

**B7 — Six-doc release alignment.** At tag time, `scripts/check_version_consistency.py` reports zero drift across CHANGELOG, README, ARCHITECTURE, PERFORMANCE, bilingual mirror docs, and package metadata per repo release discipline.

**B8 — Shared-contract claim verified.** The §3.6 `verification_gate` contract per #183 spec is satisfied for the citation gate row:
- **Source of truth** = Crossref / DOI / arXiv (external API state); satisfied by `verification_gate.verify_citation` invoking the four resolvers (Crossref + OpenAlex + S2 + arXiv).
- **Verification target** = citation exists and anchor resolves; satisfied by the `lookup_verified` (citation exists) + `anchor_present` (anchor resolves) outputs.
- **Output column** = `lookup_verified` + `anchor_present` per citation, written to the new `citation_verification_summary[]` aggregate; satisfied by `citation_verification_summary.py` per Delta 4.
- **Reviewer column** = separate from `reviewer_approved` (blocking); satisfied by Rule 12 as amended by C-V6: under `citation_existence == strict` an ID-keyed `lookup_verified == false` row (C-V6(a)) is a deterministic terminal block (`TERMINAL-BLOCK ... policy=citation_existence`) independent of any reviewer agent verdict, and the same `strict` opt-in gates `ready-for-review` (C-V6(d), symmetric with contamination). The block is governed by the opt-in policy, not by an unconditional REFUSE. Rule 12 is cross-referenced from #183 §6.1 refusal list extension table.

The `CitationVerificationOutcome` Python return type carries additional implementation-detail fields (`resolver_outcomes`, `verification_timestamp`) beyond what the §3.6 contract enumerates; those are API ergonomics, not contract surface.

**B9 — Calibration baseline preserved.** v3.10 #182 deltas do not modify `scripts/claim_audit_calibration.py` or the 20-tuple gold set at `scripts/fixtures/claim_audit_calibration/gold_set.json`. T-C1 / T-C2 / T-C3 thresholds remain green; #184's eval harness extension is the separate workstream for new gold-set tracks.

## 7. Open questions deferred to implementation

**OQ-1 — Cache TTL value.** The 90-day cache TTL is a guess. Implementation may tune based on observed Crossref / OpenAlex / S2 / arXiv metadata churn rates. Tuning is a runtime parameter (env-var configurable), not a schema change.

**OQ-2 — arXiv ID format strictness.** arXiv supports both old-style IDs (`hep-th/9711200`) and new-style IDs (`2605.07723`). Client must accept both; whether to canonicalize old-style to new-style for cache key purposes is deferred.

**OQ-3 — Multi-user cache concurrency.** Single-process SQLite WAL is adequate for v3.10 production use. If multi-user shared cache demand appears, a follow-up issue scopes the change (likely an HTTP-backed cache service, not a shared file).

**OQ-4 — `lookup_verified == unresolvable` ramp-up policy.** When every applicable resolver is `unreachable` (total outage), or when every applicable resolver is `skipped` (manual entry exempt from all four), `lookup_verified` is `unresolvable` per Delta 4. Whether this state should block ship (gate-refuse) or pass (advisory) is deferred to operational observation. v3.10 default is advisory (pass), matching v3.9.x triangulation's advisory tier. Note that the partial-outage case (some `unmatched` + some `unreachable`) is classified `false` per the Delta 4 anti-fabrication bias **only when at least one of those `unmatched` is ID-keyed** (C-V6(a)); a partial outage whose only `unmatched` are title-only resolves to `unresolvable`, not `false`. **(v3.11 amendment, C-V6(a)/(f)):** the narrowed `false` definition means the advisory-pass default never extends to citations with a provably-bogus DOI/arXiv ID, but DOES cover legitimately-unindexed (title-only-unmatched) citations, which are `unresolvable`. `unresolvable` follows this advisory default **regardless of `citation_existence`** — only ID-keyed `lookup_verified == false` is promoted to terminal under `citation_existence == strict`. So the `unresolvable` ramp-up remains genuinely open here; the existence gate's amendment does not resolve it either way.

**OQ-5 — Acknowledged recall limit of the narrowed `false` (v3.11 amendment).** The C-V6(a) narrowing (only ID-keyed `unmatched` yields `false`) is a deliberate precision-over-recall choice, and it has a named blind spot: **a fabricated citation that carries NO DOI/arXiv ID and only a bogus title resolves to `unresolvable`, not `false`, and so is NOT terminal even under `citation_existence == strict`.** This is the same recall limit `strict_articles_only` accepts by design — absence-from-index without a falsifiable identifier is treated as a coverage gap, because in the target humanities / non-English / regional-journal corpus that pattern is overwhelmingly legitimate, not fabrication. The independent design review (2026-06-02) judged this an acceptable, well-rationalized tradeoff rather than a logical hole. It is recorded here as an **Acknowledged Limitation**, not silently accepted: a no-identifier fabrication is caught by the existing v3.8 claim-faithfulness audit (anchorless / claim-not-supported routing) and by human review, not by the existence gate. A future fuzzy-title-fabrication signal (e.g. title-embedding distance against the matched corpus) could narrow this gap; it is out of scope for v3.11 and would be specced separately if the operational false-negative rate warrants it. Per repo discipline, the implementation MUST surface this limit in user-facing docs and carry a by-design false-negative fixture (no-DOI + bogus-title + strict → stays `unresolvable`, does not block), mirroring the `strict_articles_only` coverage-gap fixture.

## 8. References

- Issue #182: https://github.com/Imbad0202/academic-research-skills/issues/182
- Umbrella #198: https://github.com/Imbad0202/academic-research-skills/issues/198
- Companion full spec #183 (`verification_gate` contract source of truth): `docs/design/2026-05-21-v3.10-183-epistemic-status-spec.md`
- Companion lite spec #184 (eval harness consumer): `docs/design/2026-05-21-v3.10-184-extend-eval-harness-spec.md`
- v3.8 source-of-truth (audit pipeline + 8-row finalizer): `docs/design/2026-05-15-issue-103-claim-alignment-audit-spec.md`
- v3.9.0 triangulation (measurement layer): `docs/design/2026-05-17-ars-v3.9.0-cross-index-triangulation-measurement-spec.md`
- v3.9.4 first-party verification (pdftotext + Crossref `issued`): `shared/contracts/passport/citation_provenance.schema.json`
- v3.7.3 anchor protocol: `docs/design/2026-05-12-ars-v3.7.3-claim-faithfulness-and-contaminated-source-spec.md`
- External motivation: Zhao et al. arXiv:2605.07723 §3 — cross-index triangulation as a viable false-positive-reduction strategy for hallucinated-citation detection. Cited as reference for the triangulation design direction; production threshold decisions wait for local gold-set numbers per #184.
