Date: 2026-05-21
Issue: #182
Umbrella: #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.
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):
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.
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.
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.
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.
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) |
ARS_CLAIM_AUDIT=1. v3.10 ships five delta items below.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).
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.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.resolver_name.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.
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_existencekey (default advisory, opt-in strict), NOT anARS_CLAIM_AUDITdefault flip, and Rule 12 is thestrict-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):
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.
lookup_verified + anchor_present per-citation status surfaceToday 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).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:
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.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.
verification_gate standalone API surfacescripts/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.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.scripts/verify_passport.py, callable as python -m scripts.verify_passport <passport.yaml>).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.
scripts/arxiv_client.py (Delta 1)shared/contracts/passport/literature_corpus_entry.schema.json — contamination_signals.arxiv_unmatched field addition (Delta 1)scripts/contamination_signals.py — arxiv triangulation extension to k=0..4 advisory matrix (Delta 1)scripts/verification_cache.py (Delta 2)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)commands/ars-cache-invalidate.md — new /ars-cache-invalidate <citation_key> slash command (Delta 2)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.)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.)shared/contracts/passport/citation_verification_summary.schema.json (Delta 4)scripts/citation_verification_summary.py (Delta 4)scripts/verification_gate/__init__.py (Delta 5)scripts/verify_passport.py — standalone CLI entry point (Delta 5)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)..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.CHANGELOG.md + docs/ARCHITECTURE.md + ROADMAP §3.10 anchor — v3.10 release-alignment 6-invariant set per repo discipline.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.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).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.
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.
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.
verification_gate contract source of truth): docs/design/2026-05-21-v3.10-183-epistemic-status-spec.mddocs/design/2026-05-21-v3.10-184-extend-eval-harness-spec.mddocs/design/2026-05-15-issue-103-claim-alignment-audit-spec.mddocs/design/2026-05-17-ars-v3.9.0-cross-index-triangulation-measurement-spec.mdissued): shared/contracts/passport/citation_provenance.schema.jsondocs/design/2026-05-12-ars-v3.7.3-claim-faithfulness-and-contaminated-source-spec.md