# ARS v3.10 — Issue #183 Mandatory `epistemic_status` Field + Silent-Upgrade Detection

**Date:** 2026-05-21
**Issue:** [#183](https://github.com/Imbad0202/academic-research-skills/issues/183)
**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-182-promote-citation-gate-spec.md` (lite), `2026-05-21-v3.10-184-extend-eval-harness-spec.md` (lite)

This spec is the source of truth for the **shared `verification_gate` contract** that the other two v3.10 child specs reference. #183 is the only one of the three children that is genuinely net-new schema work; #182 and #184 promote or extend existing surface.

Per the umbrella issue gap analysis, #183 introduces:

1. A mandatory `epistemic_status` enum field on claim-bearing passport schemas (no current schema has it).
2. A source-reading extractor that assigns the status with a quoted source span.
3. A `silent_upgrade_check` lint that compares extractor-assigned status against summarizer prose register using strict ordinal `>` comparison.
4. Summarizer and writer prompt updates that render status visibly and forbid stronger-status prose unless the extractor supports it.
5. Fixtures for the specific upgrade cases described in the issue: hypothesis → causal, association → cause, in-vitro → clinical.

The verification approach is deterministic at the gate: ordinal comparison on enum values does not require an LLM judge to fire. A separate downstream LLM-judge stage (the v3.8 `synthesis_overclaim` defect class) remains as a fallback for cases where prose register escalates without a corresponding schema-field change. Section 5.4 documents the deterministic-then-judge ordering.

---

## 1. In-scope deliverables

Numbered to match implementation surfaces. All paths relative to repo root.

1. **`shared/contracts/passport/claim_intent_manifest.schema.json`** — `claims[]` item gains a required `epistemic_status` field plus a required `epistemic_status_source` object capturing the extractor's quoted span. Schema version bumps `1.0` → `2.0`.
2. **`shared/contracts/passport/claim_audit_result.schema.json`** — top-level row gains required `source_epistemic_status`, `emitted_epistemic_status`, and `silent_upgrade_detected` fields. The schema has no internal `schema_version` property (file-level versioning is via git tag); v3.10 is identified by the presence of the three new required fields per §4.2.
3. **`shared/contracts/passport/constraint_violation.schema.json`** — row gains optional `claim_epistemic_status` advisory field. Schema version bump deferred to v3.10.1 if no other change lands.
4. **`shared/contracts/passport/uncited_assertion.schema.json`** — row gains optional `claim_epistemic_status` advisory field. Same version-bump policy as constraint_violation.
5. **`shared/references/epistemic_status_vocabulary.md`** — new reference doc anchoring the 5-tier enum, ordinal rule, `Contested` sidetrack semantics, mapped appropriate-language registers, and extractor confidence levels. Cross-links to `deep-research/references/argumentation_reasoning_framework.md` (which already defines the same five labels at the reasoning-doc level; this reference doc lifts the labels into a schema-level vocabulary contract).
6. **`scripts/epistemic_status_extractor.py`** — new module: reads source paper text (corpus abstract field or pdftotext output, per §5.2 Tier policy), populates `claim_intent_manifest.claims[].epistemic_status` (claim-level aggregate) and `epistemic_status_source.per_ref_assignments[]` (per-ref tuples with `quoted_span` inside each entry). Three-tier extractor fallback (§5.2) with claim-scoped candidate-support window per §5.5.
7. **`scripts/check_silent_upgrade.py`** — new deterministic lint: for each `claim_audit_result` row, compare `source_epistemic_status` to `emitted_epistemic_status` using strict ordinal `>` on the four-tier ladder. Emit `silent_upgrade_detected: true` and feed the finalizer (Section 6). No LLM judge invocation. `Contested` rows skip the ordinal check (Section 3.3).
8. **`scripts/claim_audit_pipeline.py`** — extended with two distinct integration points reflecting the §5.1 producer-before-consumer lifecycle:
   - **Stage 4a integration (upstream of writer drafting):** new dispatch hook invokes `scripts/epistemic_status_extractor.py` against the writer's pre-commitment manifest, populating `epistemic_status` and `epistemic_status_source` on every `claims[]` entry before the writer reads the manifest.
   - **Stage 4→5 audit-boundary integration:** existing v3.8 audit pipeline gains the §5.3 emitted-status inference step (C3) and the `check_silent_upgrade.py` lint step (C4). Both run after v3.7.1/v3.7.3 anchor/locator finalization and before the v3.8 `synthesis_overclaim` judge dispatch (C5). Silent-upgrade rows skip only the `synthesis_overclaim` judge per §5.4 point 3; the `negative_constraint` judge still runs on every row per §5.7.3.
9. **`scripts/claim_audit_finalizer.py`** — `classify_claim_audit_result` gains a leading judgment-independent pre-check that returns the HIGH-WARN-SILENT-EPISTEMIC-UPGRADE annotation when `silent_upgrade_detected == true`. The v3.8 judgment-based dispatch (SUPPORTED / AMBIGUOUS / UNSUPPORTED / RETRIEVAL_FAILED) is unchanged below the pre-check. The new helper `_classify_silent_upgrade` mirrors v3.8 classify-helper return shape (dict with `annotation`, `tier`, `gate_refuse`). Per §6.2.
10. **`scripts/check_claim_audit_consistency.py`** — extends the v3.8 invariant lint to enforce v3.10 INV-19 (four-row-shape coherence: A / B / C / D per §5.7.2), INV-20 (Contested both polarities), INV-21 (negative-constraint attestation), M-INV-5 through M-INV-8 (manifest schema cross-field rules), and the v3.10 finalizer list-return shape. Existing v3.8 invariants (INV-1 through INV-18, including INV-11 polarity on `ref_retrieval_method`) retain byte-equivalent enforcement. No v3.8 enums are modified.
11. **`scripts/check_claim_audit_finalizer.py`** — companion lint enforcing the v3.10 finalizer return-shape contract per §6.2.1: `classify_claim_audit_result` returns a list of classification dicts; the list is empty only for `SUPPORTED + null` v3.8 row 1 cases; multi-element lists are valid for Shape B rows where both row 0 and v3.8 row 4 fire.
12. **`academic-paper/agents/formatter_agent.md`** — `Cite-Time Provenance Hard Gate` REFUSE list extends from rules 6-10 (v3.8) to include rule 11: `HIGH-WARN-SILENT-EPISTEMIC-UPGRADE`. The v3.8 refusal list is otherwise unchanged.
13. **`deep-research/agents/synthesis_agent.md`** + **`academic-paper/agents/draft_writer_agent.md`** + **`deep-research/agents/report_compiler_agent.md`** — each gains a new `## Epistemic Status Consumption (v3.10)` H2 section sibling to the existing v3.8 `Claim Intent Manifest Emission` and v3.7.3 `Three-Layer Citation Emission` sections. The new section requires the writer to (a) read the `epistemic_status` field already populated on each `claim_intent_manifest.claims[]` entry by the upstream extractor (see deliverable 6 and §5.1 step ordering), (b) keep emitted prose register no stronger than the assigned status's `appropriate_language` register per §3.1, and (c) never paraphrase the status enum out of the manifest under word-budget pressure (mirrors v3.6.7 protected-hedging non-negotiable budget rule). The writer is the **consumer** of `epistemic_status`, not its producer; producer ownership lives in the extractor per §5.1.
14. **`scripts/migrate_passport_to_v3_10_epistemic_status.py`** — schema migration: backfills `epistemic_status` and `epistemic_status_source` on pre-v3.10 manifests with the conservative default `Speculative` + `confidence: backfilled_low`. Backfill default rationale in Section 7.
15. **Tests** — `scripts/test_epistemic_status_schema.py` (schema validation), `scripts/test_silent_upgrade_check.py` (deterministic lint), `scripts/test_epistemic_status_extractor.py` (extractor with synthetic source fixtures), `scripts/test_claim_audit_pipeline_v3_10.py` (extended pipeline integration), `scripts/test_e2e_epistemic_status.py` (5-claim end-to-end). Fixture set per Section 8.
16. **CI wiring** — extend `.github/workflows/spec-consistency.yml` with the new lint and tests. Path filter triggers on `shared/contracts/passport/*.schema.json`, `scripts/epistemic_status_*.py`, `scripts/check_silent_upgrade.py`, and `shared/references/epistemic_status_vocabulary.md`.
17. **`CHANGELOG.md`** + **`docs/ARCHITECTURE.md`** + **`docs/PERFORMANCE.md`** + **ROADMAP §3.10 anchor** + decision-log entry. The 6-doc release-alignment invariant set (per repo release discipline) is checked at tag time by `scripts/check_version_consistency.py`.

## 2. Out of scope

Carried from the umbrella issue gap analysis and the Step C Q2 vocabulary lock. Read in conjunction with the umbrella out-of-scope list.

- **Cross-paper status reconciliation.** Meta-analysis-style combining of multiple `Preliminary` studies into a single `Supported` claim is a separate concern. Follow-up after #183 ships; not a v3.10 deliverable.
- **`evidence_type` 6-tier biomedical refinement.** Independent second-opinion review during spec drafting converged on the same verdict: reject the biomedical-6-tier proposal as a primary schema field. The cross-disciplinary 5-tier vocabulary is the right primary axis. A future follow-up issue may add an **optional** `evidence_type` field for biomedical refinement (e.g., `in_vitro_evidence`, `observational_evidence`, `causal_claim`, `validated_conclusion`), but it must not gate the primary `epistemic_status` field, must not require a `subject_domain` precondition the pipeline cannot deterministically derive, and must not be on the v3.10 critical path.
- **LLM-mediated extractor.** The Section 5.2 extractor reads source-paper text and applies a deterministic-with-citations protocol; no LLM judge is invoked at status-assignment time. v3.8 `synthesis_overclaim` judge dispatch remains the LLM-mediated downstream fallback for prose-vs-schema drift not caught by ordinal comparison.
- **`reviewer_approved` integration.** The verification gate is intentionally a separate column from human reviewer approval. Reviewer approval continues to operate independently per v3.6.2 sprint contract; this spec does not modify reviewer agent behavior.
- **Renaming the existing 5-tier reference doc.** `deep-research/references/argumentation_reasoning_framework.md` retains its name and structure. The new `shared/references/epistemic_status_vocabulary.md` is the schema-level contract; the reasoning-framework doc is the reviewer-level reasoning reference. Section 3.4 documents how the two co-exist without duplication.

---

## 3. Vocabulary contract

### 3.1 The 5-tier enum (verbatim)

The five labels and their appropriate-language registers are lifted verbatim from `deep-research/references/argumentation_reasoning_framework.md` §Epistemic Status of Claims. Schema field values are the same five strings:

| Status | Meaning | Appropriate Language |
|--------|---------|---------------------|
| `Established` | Replicated, peer-reviewed, high consensus | "X is..." |
| `Supported` | Evidence exists but not yet replicated | "Evidence suggests X..." |
| `Preliminary` | Single study or small sample | "Preliminary findings indicate..." |
| `Speculative` | Based on reasoning, not direct evidence | "We hypothesize that..." |
| `Contested` | Conflicting evidence exists | "While some studies find X, others..." |

Label strings are exact-match (case-sensitive). No alias map. The schema-level cross-field invariant lint in Section 6.5 rejects any value not in this closed set.

### 3.2 The ordinal ladder

Strict ordering on four of the five labels, used by `check_silent_upgrade.py`:

```
Established > Supported > Preliminary > Speculative
```

`Contested` is a **sidetrack meta-label** and does not participate in the ordinal comparison. Section 3.3 explains.

Implementation: ordinal values are assigned monotonically (`Speculative=1`, `Preliminary=2`, `Supported=3`, `Established=4`). The deterministic comparison in `check_silent_upgrade.py` is:

```python
def is_silent_upgrade(source: str, emitted: str) -> bool:
    if source == "Contested" or emitted == "Contested":
        return False  # sidetrack — see §3.3
    return ORDINAL[emitted] > ORDINAL[source]
```

`is_silent_upgrade` returning `true` triggers the HIGH-WARN-SILENT-EPISTEMIC-UPGRADE finalizer row (Section 6.2).

### 3.3 Why `Contested` is a sidetrack

`Contested` is not "stronger than Speculative" or "weaker than Established" — it is **orthogonal**. A `Contested` claim has two or more `Established`-level findings that disagree. Treating `Contested` as an ordinal level would force an unprincipled choice: should it be above or below `Supported`? Either choice mis-models reality.

Concrete consequence for the lint: a writer who marks the source as `Contested` and emits prose at `Established` register is **not** silent-upgrading in the ordinal sense — the prose may legitimately represent one side of the contested literature. The pipeline still benefits from the schema-level disclosure (the reader sees the `Contested` flag in the manifest), but `check_silent_upgrade.py` does not fire on these rows.

Catching one-sided treatment of `Contested` claims is **explicitly delegated to the existing v3.8 `synthesis_overclaim` judge** rather than to a new v3.10 finalizer row. The v3.8 judge is LLM-mediated and can read both the source's contestation framing and the writer's prose to decide whether the writer flattened a genuinely contested literature into a one-sided assertion. The v3.10 deterministic gate does not attempt to replicate this judgment.

Schema enforcement: `Contested` may appear in any required `epistemic_status` field. The pattern lint accepts it; the ordinal lint skips it. v3.10 does not emit a new advisory-tier finalizer row for `Contested`-register mismatch (an earlier draft of this spec promised one; the promise is withdrawn here to keep the v3.10 surface focused on the ordinal-gate decision the lint can make deterministically).

### 3.4 Co-existence with the reasoning-framework reference doc

The existing `deep-research/references/argumentation_reasoning_framework.md` is **not retired**. It continues to serve `synthesis_agent`, `devils_advocate_agent`, `source_verification_agent`, and `socratic_mentor_agent` per its existing Application by Agent table.

The new `shared/references/epistemic_status_vocabulary.md` is purposed differently: it is the **schema-level vocabulary contract** consumed by the writer agents (synthesis, draft_writer, report_compiler), the extractor, and the lint. It cites the reasoning-framework doc as the upstream definition source, but adds:

- The exact enum strings (case, spelling) used in schema fields.
- The ordinal ladder and the `Contested` sidetrack rule.
- The extractor's three-tier confidence model (Section 5.2).
- The backfill default for pre-v3.10 manifests (Section 7).
- The version policy: any future revision to the 5-tier vocabulary must bump the schema version and provide a migration script.

Single-source-of-truth invariant: the five label strings, their meanings, and their appropriate-language registers are **defined once** in the reasoning-framework doc. The new vocabulary doc backlinks to that table via section anchor; it does not duplicate the table content. The v3.10 lint at §6.4 verifies (a) the vocabulary doc carries the backlink to the canonical table location, (b) the five enum strings appear verbatim in the vocabulary doc's prose (in a non-table context — e.g., "the canonical labels are `Established`, `Supported`, `Preliminary`, `Speculative`, and `Contested`"), and (c) no second copy of the meaning-and-language table exists in the vocabulary doc. The lint is a structural check on the documentation, not a byte-equivalence check on duplicated content.

### 3.5 Orthogonality with existing claim-level fields

The `claim_intent_manifest.claims[]` schema already carries `intended_evidence_kind` (4-tier enum: `empirical | theoretical | definitional | normative`). `epistemic_status` is **a separate axis** and does not replace it:

- `intended_evidence_kind` answers: what *kind* of evidence does the writer plan to cite for this claim? (Used by the audit agent's manifest cross-reference to discriminate drift from refinement.)
- `epistemic_status` answers: how *strongly* does the source support this claim?

The two fields combine. Example: a claim may be `intended_evidence_kind: empirical` + `epistemic_status: Preliminary` (the writer plans to cite an empirical paper, but the source reports a single small-sample finding). Or `intended_evidence_kind: theoretical` + `epistemic_status: Speculative` (the writer is invoking a theoretical argument that has no empirical replication yet).

The §6.5 cross-field invariant lint requires both fields independently; no cross-field combination is forbidden.

### 3.6 `verification_gate` shared contract surface

This section is the source of truth that the two companion lite specs (`2026-05-21-v3.10-182-promote-citation-gate-spec.md` and `2026-05-21-v3.10-184-extend-eval-harness-spec.md`) reference when they say "the shared verification_gate contract per #183 §3.6." The contract is what makes the three v3.10 child issues compose into a single deterministic verification layer.

Every gate in the v3.10 layer satisfies these four columns:

| Property | #182 citation gate | #183 epistemic-status gate (this spec) | #184 lift gate |
|----------|-------------------|----------------------------------------|----------------|
| Source of truth | Crossref / DOI / arXiv (deterministic external lookup) | Source-text quoted span (extractor reads source) | Curated local gold set + recorded expert ranks |
| Verification target | Citation **exists** and anchor resolves to a verifiable locator | Claim **strength** matches the source register | Method change produces measurable lift against the gold set |
| Output column | `lookup_verified`, `anchor_present` per citation | `epistemic_status`, `silent_upgrade_detected` per claim | `lift_pre`, `lift_post` per method change, plus `expert_concordance` per task per class (the per-class shape comes from #184 §Delta 5; the per-method-change framing of the lift columns is separate from concordance, which is a gold-set property and is reported at the finer per-task-per-class granularity) |
| Reviewer column | Separate from `reviewer_approved` (the human reviewer's decision is independent of and downstream of the gate verdict) | Same separation | Same separation |

The recurring load-bearing property: **every gate's blocking column is derived from a non-LLM source.** Crossref/DOI lookups are external API state. Source-text quoted spans are read from the source paper itself (PDF or abstract field). Gold-set lift comparisons are arithmetic over recorded gold-truth values. This is the property that makes the v3.10 layer *deterministic* — no row in any of the three gates is blocked on an LLM verdict alone.

The contract does not prescribe a unified output schema. Each child gate writes its verification result into its own passport aggregate (#182 into a new `citation_verification_summary` aggregate per #182 §3 Delta 4 plus the existing v3.9.4 `citation_provenance` surface, #183 into `claim_intent_manifest` and `claim_audit_result` as specified in §4 above, #184 into a new `evals_lift_report` schema specified in #184 §3 Delta 2). The umbrella issue's acceptance criterion that each gate emits output in a shape downstream consumers can read uniformly is satisfied by adherence to the four-column contract above, not by a literal shared schema.

---

## 4. Schema migration

This section is the source of truth for which schemas change and how. The migration script (`scripts/migrate_passport_to_v3_10_epistemic_status.py`) follows the patterns in Section 7.

### 4.1 `claim_intent_manifest.schema.json` (mandatory)

Two new fields are added to each `claims[]` item, and both are added to the existing `required` array. The existing required array `["claim_id", "claim_text", "intended_evidence_kind", "planned_refs"]` extends to `["claim_id", "claim_text", "intended_evidence_kind", "planned_refs", "epistemic_status", "epistemic_status_source"]`.

```json
"epistemic_status": {
  "enum": ["Established", "Supported", "Preliminary", "Speculative", "Contested"],
  "description": "Claim-level epistemic strength assigned by the source-reading extractor (v3.10), derived from per_ref_assignments via the §5.2.6 weakest-source-wins aggregation rule. Strict 4-level ordinal among Established > Supported > Preliminary > Speculative; Contested is a sidetrack meta-label that does not participate in the ordinal lint. See shared/references/epistemic_status_vocabulary.md ordinal-ladder and contested-rule sections."
},
"epistemic_status_source": {
  "type": "object",
  "additionalProperties": false,
  "required": ["confidence", "extraction_method", "extractor_run_at", "per_ref_assignments"],
  "properties": {
    "confidence": {
      "enum": ["high", "medium", "low_no_source", "low_no_match", "backfilled_low"],
      "description": "Claim-level extractor confidence, derived as the minimum confidence over per_ref_assignments[].confidence. high = quoted span found in full-text via Tier 2 (§5.2.3). medium = quoted span found in abstract via Tier 1 (§5.2.2). low_no_source = no source text reachable for at least one ref. low_no_match = source reachable but no §5.5 heuristic matched. backfilled_low = pre-v3.10 manifest migrated by the migration script."
    },
    "extraction_method": {
      "enum": ["abstract_text", "pdftotext_full_text", "backfill_default", "low_confidence_fallback", "mixed_per_ref"],
      "description": "Which §5.2 fallback tier produced the claim-level assignment. mixed_per_ref = multiple refs in planned_refs[] used different tiers (in which case per_ref_assignments[] carries the per-ref breakdown). For single-ref claims, this value matches the sole per_ref_assignments[0].extraction_method."
    },
    "extractor_run_at": { "type": "string", "format": "date-time" },
    "per_ref_assignments": {
      "type": "array",
      "minItems": 0,
      "description": "Per-ref status assignments produced by §5.2.1. Length equals length of the parent claim's planned_refs[]; index i corresponds to planned_refs[i]. Empty array is permitted for definitional or normative claims that legitimately have no planned refs (mirrors the planned_refs[] minItems: 0 contract). When empty, the claim-level epistemic_status is set by the §7.2 conservative-default rule (Speculative + low_no_source).",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["ref_slug", "epistemic_status", "quoted_span", "source_pointer", "extraction_method", "confidence"],
        "properties": {
          "ref_slug": {
            "type": "string",
            "minLength": 1,
            "description": "The ref_slug from the parent claim's planned_refs[] this assignment corresponds to."
          },
          "epistemic_status": {
            "enum": ["Established", "Supported", "Preliminary", "Speculative", "Contested"]
          },
          "quoted_span": {
            "type": ["string", "null"],
            "minLength": 1,
            "maxLength": 1000,
            "description": "Verbatim quote from the source paper supporting this ref's assigned epistemic_status. Required (non-null) when confidence ∈ {high, medium}. MUST be null when confidence ∈ {low_no_source, low_no_match, backfilled_low}."
          },
          "source_pointer": {
            "type": ["string", "null"],
            "description": "The literature_corpus_entry.source_pointer URI the extractor dereferenced for this ref, when Tier 2 was used. Null when extraction used Tier 1 abstract only or was a Tier 3 fallback."
          },
          "extraction_method": {
            "enum": ["abstract_text", "pdftotext_full_text", "backfill_default", "low_confidence_fallback"]
          },
          "confidence": {
            "enum": ["high", "medium", "low_no_source", "low_no_match", "backfilled_low"]
          }
        }
      }
    }
  }
}
```

Schema `manifest_version` bumps from `"1.0"` to `"2.0"`. The version field's `const` constraint enforces this — any migration script run against a v1.0 manifest produces a v2.0 manifest, never an in-place mutation that leaves `manifest_version` at `"1.0"` with the new fields present.

Cross-field invariants (lint-enforced in `check_claim_audit_consistency.py`):

- **M-INV-5 (per-ref quoted_span polarity).** Within each `per_ref_assignments[]` entry: `quoted_span` MUST be non-null when `confidence ∈ {"high", "medium"}`; `quoted_span` MUST be null when `confidence ∈ {"low_no_source", "low_no_match", "backfilled_low"}`.
- **M-INV-6 (length parity).** `per_ref_assignments[]` length MUST equal the parent claim's `planned_refs[]` length, with `ref_slug` at each index matching `planned_refs[i]` exactly. Empty arrays on both sides are paired (definitional / normative claims).
- **M-INV-7 (aggregation correctness).** Claim-level `epistemic_status` MUST equal the §5.2.6 weakest-source-wins reduction over `per_ref_assignments[].epistemic_status`. Claim-level `confidence` MUST equal the minimum-confidence reduction over the §5.2.6 ordering (which includes `backfilled_low` as the weakest value). When `per_ref_assignments[]` is empty: `epistemic_status == "Speculative"` AND `confidence ∈ {"low_no_source", "backfilled_low"}` — `"low_no_source"` for claims that legitimately have no `planned_refs[]` at runtime (definitional / normative), `"backfilled_low"` for migrated empty arrays (§7.2). The split lets the lint distinguish a runtime-empty case from a migration-default case.
- **M-INV-8 (extraction_method coherence).** When `per_ref_assignments[]` is non-empty: claim-level `extraction_method == "mixed_per_ref"` iff per-ref entries use ≥ 2 distinct `extraction_method` values; otherwise claim-level matches the sole per-ref `extraction_method`. When `per_ref_assignments[]` is empty: claim-level `extraction_method == "low_confidence_fallback"` for runtime-empty (`confidence == "low_no_source"`) or `extraction_method == "backfill_default"` for migration-empty (`confidence == "backfilled_low"`). M-INV-8 lint ties the empty-array `extraction_method` value to the matching empty-array `confidence` value.

### 4.2 `claim_audit_result.schema.json` (mandatory)

Three new fields are added to each top-level row, all three are added to the existing `required` array. No v3.8 enum is modified. The existing required array extends from 13 v3.8 fields to 16 by appending `["source_epistemic_status", "emitted_epistemic_status", "silent_upgrade_detected"]`.

```json
"source_epistemic_status": {
  "enum": ["Established", "Supported", "Preliminary", "Speculative", "Contested"],
  "description": "Per-(claim, ref) status joined from claim_intent_manifest.claims[].epistemic_status_source.per_ref_assignments[i].epistemic_status, where i is the index of the row's ref_slug in the claim's planned_refs[] (per M-INV-6 length-parity). Each claim_audit_result row is per-(claim, ref); the source status for the row reflects the specific ref it points to, not the claim-level aggregate. Shape C retrieval-skipped rows use the §5.7.2 default 'Speculative'."
},
"emitted_epistemic_status": {
  "enum": ["Established", "Supported", "Preliminary", "Speculative", "Contested"],
  "description": "The status implied by the emitted draft prose register at the (claim, ref) anchored citation point, derived by the §5.3 inference. Shape C retrieval-skipped rows use the §5.7.2 default 'Speculative'."
},
"silent_upgrade_detected": {
  "type": "boolean",
  "description": "True iff is_silent_upgrade(source_epistemic_status, emitted_epistemic_status) returns true per §3.2 AND the row reached C3 inference (i.e., not Shape C). False on retrieval-skipped rows (Shape C) and on normal v3.8-dispatch rows that pass the deterministic gate (Shape D). When true, the row carries one of the two sentinel shapes (Shape A pure sentinel or Shape B sentinel-with-negative-constraint) in its judge-related columns per §5.7.2."
}
```

Per-ref vs claim-aggregate distinction (resolves the multi-ref ambiguity earlier drafts left unspecified):

- `claim_audit_result` rows are per-(claim, ref). The row's `source_epistemic_status` and `emitted_epistemic_status` are scoped to the specific ref the row points to. They are **not** the claim-level aggregate.
- The claim-level aggregate lives in `claim_intent_manifest.claims[].epistemic_status` and is computed by the §5.2.6 weakest-source-wins reduction over `per_ref_assignments[]`. It is visible to writers and reviewers but does not flow into individual audit rows.
- The deterministic comparator `is_silent_upgrade(source, emitted)` runs per-row using the row's per-ref source. This means a multi-ref claim where one ref is Established and another is Preliminary produces two distinct audit rows; the Preliminary ref's row will fire silent-upgrade if the prose register at that citation point exceeds Preliminary, but the Established ref's row will not, even if they cite the same prose sentence.

The v3.8 `ref_retrieval_method` enum is **not modified**. Shape A and Shape B sentinels preserve the row's actual v3.8 retrieval method (one of `api`, `manual_pdf`); Shape C handles paywall (`failed`) and `audit_tool_failure` cases without modifying the row's v3.8 retrieval tuple. The v3.8 INV-11 polarity (`ref_retrieval_method == "not_attempted" ↔ anchor_kind == "none"`) is preserved unchanged.

The v3.8 `defect_stage` enum is **not modified** either. The HIGH-WARN silent-upgrade class is signaled by `silent_upgrade_detected: true` (read by the §6.2 row 0 pre-check), not by a `defect_stage` value.

There is **no version field** on `claim_audit_result.schema.json` (the v3.8 schema carries no `schema_version` property; the schema file itself is versioned through git). v3.10 is identified by the presence of the three new required fields and the §5.7.2 sentinel shapes on relevant rows.

Cross-field invariants (lint-enforced, new INV-19, INV-20, INV-21):

- **INV-19 (sentinel-shape coherence).** Every `claim_audit_result` row matches exactly one of the four §5.7.2 row shapes:
  - **Shape A** (silent-upgrade only): `silent_upgrade_detected == true` AND `judgment == "AMBIGUOUS"` AND `defect_stage == null` AND `judge_model == "silent_upgrade_check_v1"` AND `ref_retrieval_method ∈ {"api", "manual_pdf"}` AND `rationale` matches the §5.7.2 Shape A template (one or two segments depending on whether constraints existed). The `failed` retrieval method is excluded — paywall rows route through Shape C.
  - **Shape B** (silent-upgrade + constraint violation): `silent_upgrade_detected == true` AND `judgment == "UNSUPPORTED"` AND `defect_stage == "negative_constraint_violation"` AND `violated_constraint_id` populated AND `judge_model == "silent_upgrade_check_v1+negative_constraint_judge_v3_8"` AND `ref_retrieval_method ∈ {"api", "manual_pdf"}` AND `rationale` matches the §5.7.2 Shape B two-segment template.
  - **Shape C** (retrieval-skipped row): `silent_upgrade_detected == false` AND `source_epistemic_status == "Speculative"` AND `emitted_epistemic_status == "Speculative"` AND the row carries an unchanged v3.8 RETRIEVAL_FAILED tuple. Four sub-cases per §5.7.2: C.1 anchorless, C.2 fabricated, C.3 paywall (`ref_retrieval_method == "failed"`), C.4 audit_tool_failure.
  - **Shape D** (normal successful row, no silent upgrade): `silent_upgrade_detected == false` AND the row's `source_epistemic_status` / `emitted_epistemic_status` reflect the per-(claim, ref) inference output (any of the five labels, not constrained to Speculative) AND the row carries an unchanged v3.8 SUPPORTED / AMBIGUOUS / UNSUPPORTED dispatch tuple. This shape covers the dominant case: a successful audit where the writer's register matches or undershoots the source register.
  Any row not matching one of the four shapes is a lint failure.
- **INV-20 (Contested sidetrack respected — both sides).** `silent_upgrade_detected == true` is forbidden whenever `source_epistemic_status == "Contested"` OR `emitted_epistemic_status == "Contested"`. Reason: `Contested` on either side skips the ordinal comparison per §3.3 / I-V3. INV-20 covers both polarities of the comparator since `is_silent_upgrade(source, emitted)` short-circuits for either-side `Contested`.
- **INV-21 (negative-constraint judge always runs, with Shape C dispatch-preservation).** Every `claim_audit_result` row whose underlying claim has any active negative constraints (manifest-level MNC or claim-level NC-C in the joinable manifest) MUST receive negative-constraint judge evaluation. Enforcement varies by shape:
  - **Shape A / B / D rows**: standard INV-21 attestation in the `rationale` field. Shape A with active constraints carries `"| negative_constraint_judge: NOT_VIOLATED ..."` segment; Shape B carries `"| negative_constraint_judge: VIOLATED ..."` segment; Shape D rows with constraint violations dispatch through the standard v3.8 UNSUPPORTED + negative_constraint_violation branch.
  - **Shape C rows (retrieval-skipped)**: the negative-constraint judge still runs, but its result MUST NOT overwrite the v3.8 RETRIEVAL_FAILED dispatch tuple (preserving v3.8 INV-6 / INV-10 / INV-11 / INV-14 polarity). On Shape C rows, the constraint judge's result is **appended to the rationale only** as `"| negative_constraint_judge_attestation: NOT_VIOLATED ..."` or `"| negative_constraint_judge_attestation: VIOLATED ({violated_constraint_id}): ..."`. The row's `judgment`, `defect_stage`, `ref_retrieval_method` columns remain at their v3.8 Shape C values regardless of constraint outcome. A VIOLATED attestation on a Shape C row surfaces an advisory line in the formatter output alongside the row's primary HIGH-WARN / LOW-WARN / MED-WARN annotation; it does not promote the row to a separate HIGH-WARN-NEGATIVE-CONSTRAINT-VIOLATION class because the row's underlying retrieval failure is the more fundamental defect the writer must fix first.
  - Rows with no active constraints (the claim's manifest entry has no MNC/NC-C) are exempt from this attestation requirement regardless of shape.

### 4.3 `constraint_violation.schema.json` (advisory)

Add optional row field:

```json
"claim_epistemic_status": {
  "enum": ["Established", "Supported", "Preliminary", "Speculative", "Contested", null],
  "description": "Advisory: the extractor's epistemic_status assignment for the violating claim, joined from claim_intent_manifest via the (scoped_manifest_id, manifest_claim_id) pair. Null when the claim was not in the manifest at extraction time. Does not gate refusal; this is informational context for the reader."
}
```

No schema version bump on this file unless paired with another change. Rationale: the field is optional and additive (existing fixtures remain valid with no change).

### 4.4 `uncited_assertion.schema.json` (advisory)

Same optional advisory field as §4.3:

```json
"claim_epistemic_status": {
  "enum": ["Established", "Supported", "Preliminary", "Speculative", "Contested", null],
  "description": "Advisory: the extractor's status for the uncited claim, joined via (scoped_manifest_id, manifest_claim_id). Null when manifest_claim_id is null (uncited sentence has no manifest binding). Does not gate refusal."
}
```

Same no-version-bump policy as §4.3.

### 4.5 Schemas explicitly unchanged

For audit completeness, the remaining four claim-related and adjacent schemas are unchanged by v3.10:

- `claim_drift.schema.json` — drift findings are detected by manifest set-diff, not by judge, and have no source-paper anchor that the extractor reads. Carrying epistemic_status on a drift row would semantically mismatch (the row represents "this claim was dropped" or "this claim appeared without a manifest entry"; status is not the right axis to describe drift).
- `uncited_audit_failure.schema.json` — UAF represents infrastructure-level judge failure, not a claim-level signal. Unchanged.
- `citation_provenance.schema.json` — citation-level Crossref/pdftotext provenance is orthogonal to claim-level status. Unchanged.
- `literature_corpus_entry.schema.json` — corpus entries describe sources, not claims. The extractor reads source data from this schema's existing fields (`abstract`, `source_pointer`); no new fields are added here. The closed-contract invariant declared in `citation_provenance.schema.json`'s description ("NOT added to literature_corpus_entry.schema.json (closed contract)") is preserved.

---

## 5. Extractor and lint

### 5.1 Pipeline placement

The v3.10 deterministic gate spans **two distinct pipeline phases**: an upstream **extraction phase** at Stage 4a (before writer drafts prose) and a downstream **lint phase** at the v3.8 Stage 4 → Stage 5 boundary (before judge dispatch). The split is load-bearing: the writer is the consumer of `epistemic_status`, the extractor is its producer, so the producer must run first.

```
Stage 4a — Extraction phase (NEW v3.10, upstream of writer drafting)
        │
        ▼
[A1] (NEW v3.10) Writer Phase 4a-1: paper-blind manifest
     skeleton emission. This re-uses the v3.6.6 paper-blind
     pre-commitment pattern already established for the
     generator-evaluator contract (per academic-paper/SKILL.md
     §v3.6.6 Phase 4a/4b split): writer agent emits manifest
     skeleton (claim_text + intended_evidence_kind + planned_refs[]
     fields only) and returns control to the orchestrator BEFORE
     paper-visible prose generation. v3.10 extends the v3.6.6
     Phase 4a contract: the manifest skeleton is the only Phase
     4a-1 output (no draft prose yet).
        │
        ▼
[A2] (NEW v3.10) Orchestrator-mediated extractor pass:
     pipeline_orchestrator_agent dispatches epistemic_status
     _extractor against the Phase 4a-1 manifest skeleton; the
     extractor mutates the manifest in place to populate
     epistemic_status + epistemic_status_source.per_ref_assignments[]
     per §5.2. Extractor is deterministic; no LLM invocation at
     this phase (per §9 invariant I-V7). The orchestrator owns
     the manifest lifecycle between A1 and A3; the writer agent
     is suspended.
        │
        ▼
[A3] Writer Phase 4a-2: orchestrator hands the populated
     manifest back to the writer with read-only access to the
     v3.10 fields (per §5.6 self-check protocol point 5; writer
     cannot edit epistemic_status). Writer resumes the v3.6.6
     Phase 4b paper-visible drafting flow, now consuming the
     extractor-assigned status as input to prose-register
     decisions per §5.6.

Stage 4b — Writer drafting (existing v3.8 Phase 4b, extended)
        │
        ▼
[B1] Writer reads each claim's extractor-assigned epistemic_status
     and the §3.1 appropriate_language register table.
        │
        ▼
[B2] Writer emits prose; §5.6 self-check keeps prose register no
     stronger than the assigned status. Writer cannot override
     epistemic_status (I-V12).

Stage 4 → Stage 5 boundary — Audit phase (extends existing v3.8 pipeline)
        │
        ▼
[C1] Manifest load + finalized prose load (existing v3.8 step 1).
        │
        ▼
[C2] v3.8 retrieval / anchor / cache resolution (existing v3.7.1
     / v3.7.3 / v3.8 §4 steps 2-4): each row is classified as
     anchorless (anchor_kind == "none"), fabricated
     (ref_retrieval_method == "not_found"), paywall, or
     retrieval-success. Rows that hit anchorless or fabricated
     gates carry their v3.8 RETRIEVAL_FAILED tuple from this
     step forward.
        │
        ▼
[C2.5] (NEW v3.10) Shape C backfill: for rows that hit a
     retrieval-side short-circuit at C2 (anchorless, fabricated,
     paywall, audit_tool_failure), write the Shape C sentinel
     into the v3.10 fields per §5.7.2 Shape C — source_epistemic
     _status = "Speculative", emitted_epistemic_status =
     "Speculative", silent_upgrade_detected = false. These
     rows then bypass C3 inference and C4 lint; they enter C5/C6
     with the v3.8 RETRIEVAL_FAILED dispatch unchanged.
        │
        ▼
[C3] (NEW v3.10) emitted-status inference (§5.3): for each
     claim_audit candidate that survived C2.5 (retrieval-success
     rows only — anchorless / fabricated / paywall / audit_tool
     _failure rows were Shape-C-backfilled at C2.5 and skip C3),
     read the finalized prose sentence and infer emitted_epistemic
     _status per §5.3 pattern catalogue. Build per-row candidate
     carrying (claim_id, ref_slug, source_epistemic_status joined
     from manifest per_ref_assignments[i] per §4.2 schema,
     emitted_epistemic_status from §5.3, anchor_kind, ref
     _retrieval_method, ...). Note: judgment is NOT populated at
     C3 — judgment is a C5 judge output. C3 reads only the
     retrieval-gate state set at C2.
        │
        ▼
[C4] (NEW v3.10) check_silent_upgrade.py reads each candidate
     row's (source_epistemic_status, emitted_epistemic_status)
     pair and writes silent_upgrade_detected per §3.2 ordinal
     comparison.
        │
        ▼
[C5] LLM judge dispatch (existing v3.8 §4 step 5), two streams:
     (a) synthesis_overclaim judge runs only when
         silent_upgrade_detected == false. Rows with
         silent_upgrade_detected == true skip this judge and
         carry §5.7.2 Shape A/B sentinel in synthesis_overclaim
         -attributable columns.
     (b) negative_constraint judge runs on EVERY row (Shape A
         /B/C and v3.8 rows alike) per §5.4 point 4 + INV-21.
         Its result attaches to the row's rationale and may
         override Shape A → Shape B if VIOLATED.
        │
        ▼
[C6] Finalizer (v3.10 row 0 prepended to v3.8 8-row matrix per
     §6.2). Row 0 fires HIGH-WARN-SILENT-EPISTEMIC-UPGRADE when
     silent_upgrade_detected == true. v3.8 rows fire per their
     unchanged tuple → annotation map. Multi-annotation rows
     return a list per §6.2.1.
        │
        ▼
[C7] formatter_agent hard gate (existing rules 1-10 + new rule 11).
```

Four load-bearing properties of this ordering:

1. **Producer-before-consumer.** Extraction runs at Stage 4a, before writer drafting at Stage 4b. The writer reads the manifest with `epistemic_status` already populated; it never produces the field itself. This resolves the temporal-ownership question.

2. **Retrieval gates before inference.** The v3.8 retrieval / anchor steps at C2 run before the v3.10 emitted-status inference at C3. Step C2.5 backfills Shape C sentinel on rows that short-circuited at C2 so they pass v3.10 schema validation without ever entering the inference. This is the rule that resolves the "required fields on skipped rows" tension: every row has Shape A, Shape B, Shape C, or Shape D, and no field can be missing on any of the four. Shapes A and B are silent-upgrade sentinels (entered at C3-C4 when the upgrade comparator fires); Shape C is the retrieval short-circuit sentinel backfilled at C2.5; Shape D is the normal successful row produced when C3-C4 ran and `silent_upgrade_detected == false`.

3. **Deterministic-before-judge.** The C3-C4 emitted-status inference and silent-upgrade lint run before the C5 LLM judge dispatch. Silent-upgrade rows skip only the `synthesis_overclaim` judge call within C5(a); the C5(b) `negative_constraint` judge runs on every row regardless of silent-upgrade status (§5.4 point 4 / §5.7.3 / INV-21).

4. **Stage 4a does not write claim_audit_result rows.** Stage 4a writes only the manifest. The Stage 4→5 audit phase (C1 onward) constructs `claim_audit_result` candidate rows for the first time. The candidate row shape is defined in §5.7 and the four canonical row shapes (A/B/C/D) live there: A and B are the silent-upgrade sentinels, C is the retrieval short-circuit sentinel, D is the normal successful row.

Rationale for the split rather than running both phases at Stage 4→5: collapsing extraction into the audit phase would force the writer to either (a) write prose without knowing each claim's source-supported status — guaranteed to produce silent upgrades — or (b) re-run the writer after extraction, doubling the LLM cost on the writing path. Pushing extraction upstream lets the writer make the status visible from the start, which the §5.6 self-check then reinforces.

### 5.2 Source-reading extractor — three-tier fallback and multi-ref aggregation

`scripts/epistemic_status_extractor.py` reads each claim in the active manifest. For each claim, the extractor produces one per-ref status assignment per entry in `planned_refs[]`, then aggregates the per-ref assignments into the claim-level `epistemic_status` per §5.2.6.

#### 5.2.1 Per-ref extraction is the unit of work

The first design decision is that the extractor's atomic unit is **per (claim, ref) pair**, not per claim. The claim-level field is derived from the per-ref array by an explicit aggregation rule. This avoids the unspecified-behavior trap of "one claim cites three sources; which source's status wins?"

For every entry in `planned_refs[]`, the extractor runs the §5.2.2-§5.2.4 three-tier fallback and produces an `(epistemic_status, quoted_span, confidence, extraction_method, source_pointer)` tuple. The full per-ref tuple array is written into `epistemic_status_source.per_ref_assignments` (see §4.1 schema). The claim-level `epistemic_status` and the claim-level `epistemic_status_source.confidence` are derived per §5.2.6.

#### 5.2.2 Tier 1 — `abstract_text`

When `literature_corpus_entry.abstract` is present and non-empty for the cited source AND no local PDF is available for this ref (per §5.2.5 Tier-choice precedence), the extractor reads the abstract text and applies the §5.5 heuristics to find quoted support for an ordinal-ladder label. If found, the per-ref tuple emits `confidence: "medium"`, `extraction_method: "abstract_text"`, `quoted_span: <verbatim quote ≤1000 chars>`.

#### 5.2.3 Tier 2 — `pdftotext_full_text`

When the corpus entry's `source_pointer` resolves to a local PDF path (`file://...pdf`), the extractor invokes a subprocess call to the system `pdftotext` binary with `-f` / `-l` page bounds capped at 50 pages. The v3.9.4 codebase carries the same subprocess pattern at `scripts/bootstrap_timeline_yaml.py:_pdftotext_first_line` (used there to read only the cover page, not the full text); v3.10 may **reuse or adapt** that pattern — extract a shared helper, copy the inline pattern, or write a separate full-text helper — at implementation time. The spec does not assume a pre-wired pdftotext verification pathway exists; v3.10 implementation builds the full-text reader. See §11 OQ-1.

The per-ref tuple emits `confidence: "high"`, `extraction_method: "pdftotext_full_text"`, `quoted_span: <verbatim quote ≤1000 chars>`.

#### 5.2.4 Tier 3 — `low_confidence_fallback`

When neither Tier 1 nor Tier 2 yields a quoted span, the per-ref tuple emits `epistemic_status: "Speculative"`, `quoted_span: null`, and one of two confidence values:

- `confidence: "low_no_source"` when no source text was reachable at all (no abstract field, no local PDF, or both unreadable).
- `confidence: "low_no_match"` when source text was reachable but no §5.5 heuristic matched anywhere in the abstract or full text.

The split lets downstream consumers distinguish "we could not read the source" from "we read the source but no heuristic engaged" — two failure modes with different remediation paths (supply better source data vs extend the heuristic catalogue).

The conservative default `Speculative` is the **floor** of the ordinal ladder; any prose register above `Speculative` will trigger silent-upgrade detection downstream, prompting the writer to either supply better source data or weaken the prose.

#### 5.2.5 Tier-choice precedence per ref (deterministic)

Per ref, the extractor follows this deterministic precedence:

1. If the corpus entry's `source_pointer` resolves to a readable local PDF (existence + read permission), run Tier 2 against that PDF. Tier 2's result is the per-ref tuple. Tier 1 is **not consulted** when Tier 2 has a readable input, regardless of whether the abstract field is present.
2. Otherwise, if `literature_corpus_entry.abstract` is non-empty, run Tier 1 against the abstract. Tier 1's result is the per-ref tuple.
3. Otherwise, emit a Tier 3 `low_no_source` tuple.

Rationale for Tier 2 unconditional preference when a PDF is available: the full text is the primary source. The abstract is a writer-shaped derivative that may compress or rephrase findings in misleading ways (the entire motivation for #183 is preventing exactly this kind of register compression). When the full text is reachable, reading the abstract instead is strictly inferior. This rule supersedes the earlier draft of this spec which described the precedence ambiguously.

#### 5.2.6 Multi-ref aggregation rule — weakest source wins

The per-ref tuple array is reduced to a claim-level `epistemic_status` by the **weakest-source-wins** rule:

- Compute the ordinal value of each per-ref `epistemic_status` per §3.2.
- Among ordinal-ladder entries (`Established / Supported / Preliminary / Speculative`), the claim-level `epistemic_status` is the value with the **lowest ordinal**.
- If at least one per-ref tuple has `epistemic_status: "Contested"` AND no per-ref tuple is on the ordinal ladder, the claim-level `epistemic_status` is `Contested` (the sidetrack).
- If at least one per-ref tuple is on the ordinal ladder AND at least one is `Contested`, the claim-level `epistemic_status` is the ordinal-ladder minimum. The `Contested` per-ref entry remains visible in `per_ref_assignments` for reviewer inspection, but does not enter the ordinal lint.

Rationale for weakest-source-wins (rather than strongest, or modal, or vote):

- **Strongest source wins** would let one strong source justify any prose register, even if other planned refs are weaker. Writers could game the lint by adding a strong reference that does not actually support the claim.
- **Modal / vote** has no clean tie-breaker for two-ref claims.
- **Weakest source wins** matches the standard academic norm: a claim is only as strong as its weakest constituent piece of evidence. If three refs are cited and one is `Preliminary`, the claim's defensible register is `Preliminary` regardless of how strong the other two are.

The claim-level `epistemic_status_source.confidence` is the minimum confidence over all per-ref tuples on the total order:

```
high > medium > low_no_match > low_no_source > backfilled_low
```

(stronger → weaker; "minimum confidence" means the rightmost value in any subset). The order places `backfilled_low` as the weakest because a migrated-default value carries less information than a runtime extraction that just failed to find a quotable span. This conservative bias forces writers to upgrade the weakest evidence rather than relying on a strong-ref average, and it makes M-INV-7 deterministic for all allowed confidence values (including the migration-default case).

#### 5.2.7 No LLM invocation at extraction time

§5.5 heuristics are a deterministic regex-and-heuristic protocol over the source text. The extractor does not call any LLM at extraction time, and the §5.2.6 aggregation rule is pure arithmetic on enum values. This is the load-bearing property that makes the silent-upgrade detection deterministic (per invariant I-V7).

### 5.3 Emitted-status inference

`emitted_epistemic_status` in each `claim_audit_result` row is computed from the emitted draft prose register, not from a writer-declared field. The inference is **mechanical** and applies a deterministic **outer-frame-wins** precedence over a layered pattern catalogue. The outer-frame rule is load-bearing: the previous draft of this spec used a "strongest unqualified assertion wins" rule that produced systematic false positives on standard academic prose like "Evidence suggests X is associated with Y" (which a naive `X is` matcher would classify as `Established`).

#### 5.3.1 Pattern layers (evaluation order is fixed)

The catalogue has five layers. The inference engine walks them **in this fixed order** per sentence; the first layer that matches wins.

**Layer 1 — Contestation frame.** Sentence-level contestation markers: `while some studies find X, others find`, `the literature disagrees`, `findings are mixed`, `competing accounts`, `inconsistent across`. Match → emitted register = `Contested`. Layer 1 runs first so contestation prose containing internal unqualified copulas (e.g., "while some studies find that X is the cause, others...") does not get mis-categorized by lower layers.

**Layer 2 — Attribution frame.** Reported-speech markers: `Smith et al. claim`, `<author>... argue`, `<author>... posit`, `<author>... assert`, `according to`, `as <author>... write`. When an attribution frame is detected, the inference engine extracts the attributed clause and classifies the **attribution as a whole** as `Speculative`. The unqualified language inside the attributed clause does not promote the register. (This is the writer's intended attribution escape per §5.6 point 4.)

**Layer 3 — Hypothesis frame.** First-person hypothesis or proposal markers: `we hypothesize`, `we propose`, `we conjecture`, `we predict`, `we speculate`. Match → emitted register = `Speculative`. Layer 3 catches the writer's own explicit hypothesis prose; this layer's presence at the head of a sentence beats internal copulas downstream.

**Layer 4 — Inferential / modal hedge frame.** Hedge markers at the head of the sentence or immediately preceding the substantive claim: `Evidence suggests`, `results indicate`, `findings are consistent with`, `may` / `might` / `could`, `it appears that`, `preliminary findings indicate`, `in our sample`, `for this cohort`, `under the conditions tested`. The match scope is **the sentence containing the marker**: any unqualified copula or causal verb downstream of the marker in the same sentence is considered scoped to the hedge.

When Layer 4 matches a sentence-head hedge, the emitted register is the **register associated with the hedge type**:

- Modal hedges (`may`, `might`, `could`) → emitted register = `Preliminary` (the writer signals reduced confidence on a claim).
- Preliminary-finding markers (`preliminary findings`, `pilot study`, `single-site study`, `in this exploratory cohort`) → emitted register = `Preliminary`.
- Inferential hedges (`Evidence suggests`, `results indicate`, `findings are consistent with`) → emitted register = `Supported` (the writer attributes the claim to evidence, not asserts it directly).
- Scope hedges (`in our sample`, `for this cohort`, `under the conditions tested`) → emitted register = `Supported`.

The matching is greedy: a single sentence containing both modal and inferential markers gets the lower-register classification (modal → Preliminary wins).

**Layer 5 — Unqualified assertion (fallback).** Only reached when Layers 1-4 did not match. Detects unqualified copulas and causal verbs: `X is`, `X causes`, `X demonstrates`, `X establishes`, `X proves`, `X validates`, `X is the cause of`. Match → emitted register = `Established`. The fallback layer catches prose with no hedge or attribution scope.

#### 5.3.2 Why outer-frame-wins not strongest-wins

The previous spec draft said "strongest unqualified assertion wins" when multiple patterns match in the same sentence. That rule has two pathological cases:

- "Evidence suggests X is associated with Y" → `X is` matches Layer 5 → classified as `Established` even though the sentence-head `Evidence suggests` makes the claim clearly `Supported`. The lint would fire false-positive silent upgrades on basic academic prose.
- "Smith et al. claim that X is the dominant mechanism" → `X is` matches Layer 5 → classified as `Established` even though the writer attributed the claim and did not endorse it. The lint would fire false-positive upgrades on standard literature-review writing.

Outer-frame-wins inverts the precedence: outer scope (contestation / attribution / hypothesis / hedge frames) dominates inner content (copulas / causal verbs). False negatives are still possible — a writer could embed a strong claim inside a weak frame in a way the inference engine misses — but false positives on standard academic prose are eliminated. The §5.4 LLM judge composition catches the residual false negatives.

#### 5.3.3 Pattern catalogue location and versioning

The full pattern lists for each layer (including regex flavors, word-boundary rules, and case-folding behavior) live in `shared/references/epistemic_status_vocabulary.md` §Inference Patterns and are consumed by `check_silent_upgrade.py`. New patterns may be added to any layer in patch releases without bumping schema versions, since the schema enum values themselves do not change. However, adding a pattern to a higher-precedence layer can change which layer matches first on previously-classified text — a sentence that previously fell to Layer 5 (Established fallback) may newly match a Layer 4 hedge frame and be classified as Supported. Patch releases that add patterns MUST review the v3.10 fixture set and update expected `emitted_epistemic_status` values where the new pattern matches earlier than the previous catalogue allowed. This is the writer-side projection of I-V8; the invariant is enforced by the fixture-expected-output diff at PR review time. Pattern **removal** requires a schema version bump and a migration script.

### 5.4 Composition with v3.8 `synthesis_overclaim` judge

The v3.8 `claim_audit_result.defect_stage == "synthesis_overclaim"` defect class continues to operate as the LLM-mediated downstream fallback. The two layers compose:

1. **#183 deterministic gate first.** `check_silent_upgrade.py` reads `(source_epistemic_status, emitted_epistemic_status)` and applies strict ordinal `>` comparison. When the gate fires (e.g., source `Preliminary`, emitted `Established`), the finalizer's judgment-independent pre-check at §6.2.1 emits HIGH-WARN-SILENT-EPISTEMIC-UPGRADE. The `synthesis_overclaim` judge does not run for this row.

2. **v3.8 judge second.** For rows the deterministic gate does not catch — e.g., source and emitted both `Supported` at the schema-enum level but the prose register subtly escalates beyond what the source supports in a way the §5.3 inference catalogue does not match — the v3.8 judge fires per its existing `claim_ref_alignment_audit_agent.md §4` step 5 prompt. The judge may return `defect_stage: synthesis_overclaim` on rows where the deterministic lint already passed, and the row routes through the v3.8 HIGH-WARN-CLAIM-NOT-SUPPORTED class via the v3.8 UNSUPPORTED branch (not the new HIGH-WARN-SILENT-EPISTEMIC-UPGRADE).

3. **No `synthesis_overclaim` double-counting.** A row that produces `silent_upgrade_detected == true` skips the `synthesis_overclaim` judge specifically, so it cannot also receive `defect_stage: synthesis_overclaim`. The two HIGH-WARN classes are mutually exclusive at the row level.

4. **Negative-constraint judge runs on every row.** The §5.4 point 3 skip is **scoped to the `synthesis_overclaim` judge only**. The negative-constraint judge (v3.8 §4 step 5 stream (d) NC-C/MNC evaluation) is a separate judge call governed by §5.7.3 and INV-21; it runs on every candidate row regardless of `silent_upgrade_detected`. When the negative-constraint judge fires on a silent-upgrade row, the row carries §5.7.2 Shape B sentinel and both HIGH-WARN annotations surface on the formatter output per the §6.2.1 list-based composition rule.

5. **Aggregate visibility.** The Stage 6 histograms (extended per §6.3) report both classes separately, so the user can see (a) how often the deterministic gate fires, (b) how often the judge catches register drift the deterministic gate misses. Over time, gap (b) measures the deterministic gate's recall ceiling.

### 5.5 Source-reading heuristics

The extractor applies these heuristics to a **claim-scoped candidate-support window** within the source text, not to the full source document. The window is computed deterministically per `(claim_id, ref_slug)` pair:

1. **Tokenize** the claim's `claim_text` (from the manifest) and the source text (abstract or full text per §5.2 Tier policy) into whitespace-split lowercase tokens.
2. **Compute claim-term tokens**: the claim's tokens minus a stop-word list (`a / an / the / of / in / on / and / or / for / to / with / is / are / was / were`).
3. **Locate the candidate-support window**: a contiguous span of 3 source sentences centered on the source sentence with the highest overlap count (number of claim-term tokens that appear in that sentence). If no source sentence has overlap ≥ 1 token, the extractor emits Tier 3 fallback (`Speculative` + `low_no_match`) without running H1-H5.
4. **Apply H1-H5** to the candidate-support window (3 sentences), not the full source. First-match-wins as described below.

The 3-sentence window is the deterministic compromise between (a) catching the claim's actual evidential context (one-sentence windows miss multi-sentence reasoning), and (b) avoiding the false-positive trap where a single unrelated "we hypothesize" sentence elsewhere in the paper propagates to every claim. The token-overlap rule is the only LLM-free way to scope source reading to the specific claim/ref pair.

Within the windowed text, the first heuristic whose substring or regex pattern matches anywhere in the window wins; no confidence scoring or threshold is applied. Heuristics are simple text-presence tests, not graded matchers — this is what keeps the extractor deterministic (I-V7).

Heuristics are evaluated in this fixed order; first match wins. The ordering mirrors the §5.3 emitted-status outer-frame-wins precedence: contestation framing is checked first so a sentence like "replicated findings are inconsistent across cohorts" gets `Contested` rather than `Established`.

**H1 — Contestation markers.** Source contains "while some studies find X, others find Y", "the literature is divided", "findings are inconsistent across", "competing accounts", or "no consensus on" → `Contested`. Quoted span = the matching sentence. H1 runs first so contestation prose containing internal replication or association language does not get mis-categorized.

**H2 — Hypothesis / theoretical-argument markers.** Source contains "we hypothesize", "we propose", "we conjecture", "based on the theoretical framework of", or "extrapolating from" → `Speculative`. Quoted span = the matching sentence. H2 runs before H3-H5 so an explicit hypothesis frame ("we hypothesize that meta-analysis supports X") is classified as Speculative, not as Established by the embedded meta-analysis marker.

**H3 — Single-study / small-sample markers.** Source contains "pilot study", "preliminary findings", "in this exploratory cohort", "small sample (n < 30)", or "single-site study" → `Preliminary`. Quoted span = the matching sentence. Runs before H4 / H5 so a small-sample claim is not promoted by an embedded "evidence suggests" marker.

**H4 — Evidence-suggests pattern.** Source contains "evidence suggests", "results indicate", "findings are consistent with", or "demonstrates an association" → `Supported`. Quoted span = the matching sentence.

**H5 — Explicit replication language (fallback).** Source contains "replicated", "meta-analysis confirms", "consistent across N independent samples", or "consensus in the field" → `Established`. Quoted span = the matching sentence. H5 runs last because it is the only heuristic that promotes to the top of the ordinal ladder; deferring it ensures any outer-frame qualifier (contestation, hypothesis, small-sample, hedge) wins first.

**Default when no heuristic matches.** When the source text is reachable (Tier 1 or Tier 2 succeeded in providing readable text) but no H1-H5 pattern matches, the extractor emits `Speculative` with `confidence: "low_no_match"` and `quoted_span: null`. (When the source text is not reachable at all — no abstract, no local PDF — the extractor emits `confidence: "low_no_source"` instead per §5.2.4.) Rationale: the conservative default treats unmatchable sources as the weakest claim, forcing the writer to either find better source support or weaken the prose; the two-value split lets downstream consumers distinguish "we read the source but no heuristic engaged" from "we could not read the source."

The pattern strings are not exhaustive; the catalogue extends in patch releases via additions to the H1-H5 lists. The single load-bearing invariant is that **every pattern is a deterministic regex or simple substring match** — no LLM disambiguation at extraction time.

### 5.6 Writer-side pre-emission self-check

The new `## Epistemic Status Consumption (v3.10)` section added to `synthesis_agent`, `draft_writer_agent`, and `report_compiler_agent` instructs the writer to perform a self-check before emitting each claim. By the time the writer runs, the upstream extractor (Stage 4a per §5.1) has already populated `epistemic_status` and `epistemic_status_source` on every manifest claim. The writer reads these fields and adjusts prose accordingly.

1. Read the extractor-populated `epistemic_status` for the claim from the active manifest. The field is mandatory at this phase (the manifest has already gone through Stage 4a extraction); a missing or unrecognized value is a contract violation, not a writer-side fallback case.
2. Read the appropriate-language register for that status (§3.1 table).
3. Adjust the prose to **not exceed** that register. Concretely: a `Preliminary` claim must not use "X is", "X causes", "X demonstrates"; instead, "Preliminary findings indicate X" or "in this exploratory cohort, X".
4. When the prose must use stronger language for grammatical or rhetorical reasons (rare; e.g., the claim is being **reported as quoted** from the source rather than asserted by the writer), the writer wraps the strong language in attribution: `Smith et al. claim that X is...`. The §5.3 inference treats reported speech as `Speculative` (attributed assertion, not endorsed assertion).
5. The writer never **overrides** the extractor-assigned status. If the writer believes the extractor mis-assigned a claim, the resolution is to (a) revisit the source paper, (b) re-run the upstream extractor with the source corrected (e.g., a missing full-text PDF added), or (c) escalate to the user via the v3.7.0 `claim-faithfulness-low-confidence` advisory. The writer cannot edit `epistemic_status` directly. This rule is the writer-side projection of I-V12.

The self-check is in the writer prompt, not in a runtime lint, because the downstream silent-upgrade lint catches violations the writer missed. The self-check reduces the false-positive rate of the lint (writers who follow the self-check produce drafts that the lint approves on first pass).

### 5.7 Candidate row contract + sentinel tuple for silent-upgrade rows

The §5.1 step C3 candidate-row construction and the §5.1 step C5 judge-skipping rule together require two formal contracts:

#### 5.7.1 Pre-judge candidate row shape

Before the C5 judge dispatch, each candidate `claim_audit_result` row carries the columns the deterministic gate (C4) needs to make its decision. These columns are:

- `claim_id`, `scoped_manifest_id`, `claim_text` (carried forward from the manifest).
- `ref_slug`, `anchor_kind`, `anchor_value` (carried forward from the v3.7.3 anchor markers and v3.7.1 cite-time finalizer).
- `source_epistemic_status` (carried forward from `claim_intent_manifest.claims[].epistemic_status` per the (scoped_manifest_id, claim_id) join).
- `emitted_epistemic_status` (computed at step C3 by the §5.3 inference).
- `silent_upgrade_detected` (computed at step C4).

Columns the judge would normally populate (`judgment`, `audit_status`, `defect_stage`, `rationale`, `judge_model`, `judge_run_at`) are absent at the C3-C4 phase, **except `ref_retrieval_method`** which is populated by the v3.8 retrieval step C2 before C3 inference runs (correcting an earlier draft of this sub-section that listed it as absent). The schema validator does not run on the candidate row; the candidate is an in-memory intermediate that exists only between C3 and C6. The full `claim_audit_result` schema validation runs after C6 (finalizer matrix output assembly), at which point either the C5 judge has populated the judge-required columns or one of the §5.7.2 sentinel shapes has been applied.

#### 5.7.2 Row shapes — four valid configurations

Every `claim_audit_result` row matches exactly one of four shapes, distinguished by whether the v3.10 silent-upgrade gate fired and whether retrieval gates short-circuited the inference. INV-19 at §4.2 / §6.5 lint-enforces the four-way partition.

##### Shape A — Pure sentinel (silent-upgrade only, no constraint violation)

Applied when `silent_upgrade_detected == true` AND the negative-constraint judge returned NOT_VIOLATED on the row (or no active constraints exist for the claim).

| Column | Sentinel value | Rationale |
|--------|---------------|-----------|
| `judgment` | `"AMBIGUOUS"` | The deterministic gate cannot adjudicate beyond detecting upgrade. AMBIGUOUS is the closest existing v3.8 enum value for "decision deferred." |
| `audit_status` | `"completed"` | The deterministic gate completed its evaluation. |
| `defect_stage` | `null` | The HIGH-WARN class is signaled by `silent_upgrade_detected: true`, not by `defect_stage`. `null` is permitted by the v3.8 `defect_stage` enum on AMBIGUOUS rows. |
| `rationale` | Two-segment string: `"silent_upgrade_detected by check_silent_upgrade.py: source={source_status}, emitted={emitted_status}"` + (when active constraints exist) `" | negative_constraint_judge: NOT_VIOLATED ({constraint_ids_evaluated})"`. The second segment is appended verbatim by the constraint judge run; when no constraints exist for the claim, the rationale carries only the first segment. | Two-segment design satisfies both the v3.10 silent-upgrade rationale contract AND the INV-21 attestation requirement that constraint-judge results be carried on the row. The `|` separator is the canonical sentinel delimiter. |
| `judge_model` | `"silent_upgrade_check_v1"` (lint module + version) | v3.8 `judge_model` field accepts any non-empty string. The lint's identifier is the synthetic-judge attribution. |
| `judge_run_at` | The lint's run timestamp (ISO-8601 UTC) | Matches v3.8 judge timestamp format. |
| `ref_retrieval_method` | The row's actual retrieval method as set by v3.8 retrieval step C2, restricted to `{"api", "manual_pdf"}` | The `failed` (paywall) case is excluded because v3.8 INV-10 requires `failed → RETRIEVAL_FAILED + inconclusive + not_applicable`, which conflicts with Shape A's `AMBIGUOUS + completed + null` tuple. Paywall rows route through Shape C.3 instead. |

##### Shape B — Sentinel with negative-constraint violation (both fire)

Applied when `silent_upgrade_detected == true` AND the negative-constraint judge returned VIOLATED on the row.

| Column | Override value | Rationale |
|--------|----------------|-----------|
| `judgment` | `"UNSUPPORTED"` | v3.8 INV-7 requires `judgment == "UNSUPPORTED"` paired with `defect_stage == "negative_constraint_violation"`. The override preserves this v3.8 invariant. |
| `defect_stage` | `"negative_constraint_violation"` | Per v3.8 INV-7. |
| `violated_constraint_id` | The constraint ID that fired | Per v3.8 INV-7. |
| `rationale` | Two-segment string: `"silent_upgrade_detected by check_silent_upgrade.py: source={source_status}, emitted={emitted_status}"` + `" | negative_constraint_judge: VIOLATED ({violated_constraint_id}): {constraint_judge_rationale}"` | Same two-segment design as Shape A. The first segment is the v3.10 silent-upgrade signal; the second is the v3.8 constraint judge rationale. Both annotations surface on the formatter output per §6.2.1. |
| `judge_model` | `"silent_upgrade_check_v1+negative_constraint_judge_v3_8"` (concatenated attribution) | Acknowledges two synthetic-judge sources contributed to the row. |
| `judge_run_at` | The later of the two timestamps (silent-upgrade lint run, constraint judge run) | One row → one timestamp; takes the latter so the row is timestamped as "fully evaluated." |
| `ref_retrieval_method` | As in Shape A — restricted to `{"api", "manual_pdf"}` | Same INV-10 exclusion as Shape A; paywall rows route through Shape C.3. |

##### Shape C — Retrieval-gate short-circuit (silent-upgrade did NOT fire)

Applied to rows where the v3.8 retrieval / anchor gates short-circuited the v3.10 emitted-status inference. These rows carry `silent_upgrade_detected == false` (the inference never ran). Required to keep the schema's mandatory `source_epistemic_status`, `emitted_epistemic_status`, `silent_upgrade_detected` fields populated on rows that bypass C3 inference.

Four sub-cases, each preserving the row's unchanged v3.8 RETRIEVAL_FAILED dispatch tuple:

**Shape C.1 — Anchorless** (`anchor_kind == "none"`, v3.8 row 6): `source_epistemic_status = "Speculative"`, `emitted_epistemic_status = "Speculative"`, `silent_upgrade_detected = false`. v3.8 retrieval columns unchanged: `ref_retrieval_method = "not_attempted"` per v3.8 INV-11, `judgment = "RETRIEVAL_FAILED"`, `audit_status = "inconclusive"`, `defect_stage = "not_applicable"`. v3.8 row 6 dispatches normally and emits HIGH-WARN-CLAIM-AUDIT-ANCHORLESS.

**Shape C.2 — Fabricated reference** (`ref_retrieval_method == "not_found"`, v3.8 row 5): same emitted-status defaults as C.1. v3.8 retrieval columns: `judgment = "RETRIEVAL_FAILED"`, `defect_stage = "retrieval_existence"`, `audit_status = "inconclusive"`. v3.8 row 5 dispatches normally and emits HIGH-WARN-FABRICATED-REFERENCE.

**Shape C.3 — Paywall** (`ref_retrieval_method == "failed"`, v3.8 row 7): same emitted-status defaults as C.1. v3.8 retrieval columns: `judgment = "RETRIEVAL_FAILED"`, `defect_stage = "not_applicable"`, `audit_status = "inconclusive"` (per v3.8 INV-10). v3.8 row 7 dispatches normally and emits `[CLAIM-AUDIT-UNVERIFIED — REFERENCE FULL-TEXT NOT RETRIEVABLE]` LOW-WARN advisory (gate passes). The deterministic v3.10 gate cannot read paywalled sources at all, so silent-upgrade detection is structurally impossible; the row stays at the v3.8 paywall advisory tier.

**Shape C.4 — Audit-tool failure** (`ref_retrieval_method == "audit_tool_failure"`, v3.8 row 8): same emitted-status defaults as C.1. v3.8 retrieval columns: `judgment = "RETRIEVAL_FAILED"`, `defect_stage = "not_applicable"`, `audit_status = "inconclusive"` (per v3.8 INV-14). v3.8 row 8 dispatches normally and emits `[CLAIM-AUDIT-TOOL-FAILURE — <fault-class>]` MED-WARN advisory. Retry-next-pass remediation applies; the v3.10 inference will run on the next pass once the tool failure clears.

Shape C is a **defensive backfill**, not an active gate verdict. It ensures the three v3.10 required fields are present on rows the inference did not visit; downstream consumers reading these rows see `silent_upgrade_detected == false` and skip the v3.10-specific logic. The §5.1 step C2.5 explicitly applies Shape C to retrieval-gated rows before C3 inference begins, so C3 never sees these rows.

##### Shape D — Normal successful row (silent-upgrade gate passed, real statuses)

Applied to rows that reached C3 inference, passed the C4 deterministic gate (`silent_upgrade_detected == false`), and received normal v3.8 judge dispatch at C5. This is the **dominant case** in production: the writer's prose register matches or undershoots the source register.

| Column | Value |
|--------|-------|
| `source_epistemic_status` | Per-(claim, ref) value joined from manifest `per_ref_assignments[i].epistemic_status` (any of the five labels, not constrained to Speculative) |
| `emitted_epistemic_status` | Per-(claim, ref) value computed by §5.3 inference (any of the five labels) |
| `silent_upgrade_detected` | `false` |
| `judgment` | Any v3.8 judge verdict: `SUPPORTED` / `AMBIGUOUS` / `UNSUPPORTED` / `RETRIEVAL_FAILED` |
| `defect_stage`, `audit_status`, `rationale`, `judge_model`, `judge_run_at`, `ref_retrieval_method` | Whatever the v3.8 judge dispatch produces; no v3.10 sentinel overrides |

Shape D rows do not require any v3.10-specific lint beyond the polarity of `silent_upgrade_detected` (which must be `false` when the comparator returns `false` per §3.2). The v3.8 finalizer dispatches Shape D rows through the v3.8 8-row matrix as if v3.10 were not present. Most production rows are Shape D.

INV-19 (§4.2) lint-enforces the four-shape partition: every row matches exactly one of A / B / C / D, and no row matches more than one. Lint test fixtures cover one row per shape.

#### 5.7.3 Negative-constraint judge always runs

The §5.4 point 4 rule scopes the C5 judge skip narrowly: only the `synthesis_overclaim` judge is skipped when `silent_upgrade_detected == true`. The negative-constraint judge (v3.8 §4 step 5 stream (d) NC-C/MNC evaluation) runs on every candidate row regardless of `silent_upgrade_detected`. This is INV-21 stated at the spec level.

Composition outcomes (mapped to row shapes):

- **Silent-upgrade only, no active constraints.** Row carries Shape A with single-segment rationale. Row 0 of the §6.2 finalizer fires HIGH-WARN-SILENT-EPISTEMIC-UPGRADE.
- **Silent-upgrade + constraint NOT_VIOLATED.** Row carries Shape A with two-segment rationale (NOT_VIOLATED attestation appended). Row 0 fires HIGH-WARN-SILENT-EPISTEMIC-UPGRADE.
- **Silent-upgrade + constraint VIOLATED.** Row carries Shape B. Row 0 fires HIGH-WARN-SILENT-EPISTEMIC-UPGRADE; v3.8 row 4 (UNSUPPORTED + negative_constraint_violation) also fires HIGH-WARN-NEGATIVE-CONSTRAINT-VIOLATION. Both annotations surface per §6.2.1 list-based composition. The writer must address both to ship.
- **Constraint-violation only, no silent upgrade, successful retrieval.** `silent_upgrade_detected == false` AND retrieval-success. Row is Shape D (judgment == UNSUPPORTED, defect_stage == negative_constraint_violation, real status values from per-ref join). Row 0 does not fire; v3.8 row 4 emits HIGH-WARN-NEGATIVE-CONSTRAINT-VIOLATION.
- **Successful audit row, no silent upgrade, no constraint violation.** `silent_upgrade_detected == false`, no negative_constraint_violation. Row is Shape D with normal v3.8 dispatch (SUPPORTED / AMBIGUOUS without constraint / etc.). Row 0 does not fire; v3.8 finalizer handles dispatch normally.
- **Retrieval-gated row** (anchorless / fabricated / paywall / audit_tool_failure). Row carries Shape C with the appropriate sub-case (C.1-C.4); `silent_upgrade_detected == false`. Row 0 does not fire. v3.8 dispatch emits the row's primary annotation (HIGH-WARN-CLAIM-AUDIT-ANCHORLESS, HIGH-WARN-FABRICATED-REFERENCE, LOW-WARN paywall, or MED-WARN audit_tool_failure). If the negative-constraint judge fires on a Shape C row, its result is **appended to rationale as an attestation line only** (per §4.2 INV-21 Shape C dispatch-preservation rule); the row's v3.8 dispatch tuple is not overwritten. A Shape C row with both a VIOLATED constraint and (say) ANCHORLESS surfaces both annotations in formatter output — the ANCHORLESS as primary and the constraint as advisory-line.

This design satisfies all four invariants that earlier drafts of this section could not jointly satisfy: schema required fields are always populated (Shape C handles bypass cases, Shape D handles successful cases); silent-upgrade rows skip only synthesis_overclaim judge (Shape A / B keep `synthesis_overclaim`-attributable columns at sentinel values); negative-constraint judge always runs and its result is always carried on the row (INV-21 via two-segment rationale or Shape C attestation line); multi-annotation surfacing is enabled (Shape B + §6.2.1 list return).

---

## 6. Finalizer matrix, formatter, and lint wiring

### 6.1 Formatter refusal list extension

`academic-paper/agents/formatter_agent.md` Cite-Time Provenance Hard Gate REFUSE list extends from v3.8's 10 rules to 12:

- Rules 1-5 (v3.7.1/v3.7.3): unchanged.
- Rules 6-10 (v3.8 HIGH-WARN classes): unchanged.
- **Rule 11 (NEW v3.10, owned by this spec #183):** Refuse on any `claim_audit_results[]` row where `silent_upgrade_detected == true`. Refusal annotation: `[HIGH-WARN-SILENT-EPISTEMIC-UPGRADE — claim C-{n} in manifest M-{ts}-{hex}: source={source_status}, emitted={emitted_status}]`.
- **Rule 12 (NEW v3.10, owned by companion spec #182 Delta 3):** Refuse on any `citation_verification_summary[]` row where `lookup_verified == false`. 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}]]`. See `docs/design/2026-05-21-v3.10-182-promote-citation-gate-spec.md` §Delta 3 + §B8 for the owning surface. The `unresolvable` outcome does NOT trigger Rule 12 per #182 OQ-4 advisory-pass default.

Existing CI lint `scripts/check_v3_7_3_three_layer_citation.py` and the v3.8 equivalent are not modified. A new `scripts/check_v3_10_silent_upgrade_refusal.py` adds set-equality enforcement for Rule 11; a parallel new `scripts/check_v3_10_lookup_verified_refusal.py` adds set-equality enforcement for Rule 12 (lint module name owned by #182 implementation; this spec references the lint contract only).

### 6.2 Finalizer extension — new row 0 prepended to the v3.8 8-row matrix

v3.8 documents its finalizer as an **8-row top-to-bottom first-match-wins matrix** (per v3.8 spec §5; see `docs/design/2026-05-15-issue-103-claim-alignment-audit-spec.md`). The matrix dispatches on `(judgment, defect_stage, ref_retrieval_method)` tuples. The current Python implementation at `scripts/claim_audit_finalizer.py:classify_claim_audit_result` realizes the matrix as a judgment-based `if`-chain — that is an idiomatic Python encoding of the row-based contract, not a different contract. The contract is what this spec extends; the encoding is an implementation detail.

v3.10 extends the matrix by **prepending a new row 0** evaluated before the v3.8 rows 1-8. The full v3.10 matrix has 9 rows; v3.8 rows shift to positions 1-9.

Row 0 (NEW v3.10):

| Position | Condition | Annotation | Severity Tier | Gate behavior |
|----------|-----------|------------|---------------|---------------|
| **0** (NEW) | `silent_upgrade_detected == true` | `[HIGH-WARN-SILENT-EPISTEMIC-UPGRADE — claim {claim_id} in manifest {scoped_manifest_id}: source={source_epistemic_status}, emitted={emitted_epistemic_status}]` | HIGH-WARN | gate-refuse |
| 1 | (was v3.8 row 1) `judgment == "SUPPORTED" AND defect_stage == null` | (no annotation) | — | pass |
| 2-8 | (was v3.8 rows 2-8) unchanged tuple → annotation mapping | — | — | — |

Row 0 reads only the v3.10 `silent_upgrade_detected` boolean; it does not read `judgment`, `defect_stage`, or `ref_retrieval_method`. This keeps the new gate's decision orthogonal to the v3.8 judge taxonomy.

#### 6.2.1 Multi-annotation row classification

Row 0 wins under first-match-wins, but **silent-upgrade and negative-constraint violation can both fire on the same row**, and both must surface in the formatter output. v3.8 rows are mutually exclusive on a single row (one tuple → one annotation), but v3.10 introduces the first case where two rows logically fire simultaneously.

To accommodate this, `classify_claim_audit_result` returns a **list of classification dicts** rather than a single dict (this is a v3.10 return-shape extension to the v3.8 helper). v3.8 callers that previously read a single dict are updated in the same v3.10 release to iterate the list and apply each annotation; legacy single-dict callers continue to work via a backwards-compatibility shim that returns the list's first element when called via the v3.8 single-return entry point (preserved as `classify_claim_audit_result_single`).

Algorithm:

```python
def classify_claim_audit_result(entry: dict) -> list[dict]:
    annotations: list[dict] = []

    # Row 0 (NEW v3.10): silent-upgrade gate fires.
    if entry.get("silent_upgrade_detected") is True:
        annotations.append(_classify_silent_upgrade(entry))

    # v3.8 rows 1-8 dispatched as in v3.8 §5 matrix; defect_stage may carry
    # negative_constraint_violation alongside the §5.7.2 sentinel, so a Shape B
    # row contributes BOTH the row-0 silent-upgrade annotation above AND the
    # negative-constraint annotation from the v3.8 UNSUPPORTED branch below.
    v3_8_annotation = _classify_v3_8_matrix(entry)
    if v3_8_annotation is not None:
        annotations.append(v3_8_annotation)

    return annotations
```

Behavioral consequences:

- **Pure silent-upgrade row (Shape A sentinel).** `_classify_v3_8_matrix` evaluates the row against the Shape A tuple (`judgment == "AMBIGUOUS"`, `defect_stage == null`). The v3.8 AMBIGUOUS row matches and emits `[CLAIM-AUDIT-AMBIGUOUS]` LOW-WARN — but this is a contract conflict with the row-0 HIGH-WARN gate-refuse. **Special case in `_classify_v3_8_matrix`:** when the row's `judge_model == "silent_upgrade_check_v1"` (the §5.7.2 sentinel marker), the v3.8 AMBIGUOUS classification is suppressed (returns `None`), since the AMBIGUOUS annotation would duplicate-with-different-tier the row-0 annotation. This rule is the only v3.8 dispatch behavior the spec modifies; it is added to `_classify_v3_8_matrix` as an early-return guard documented in deliverable 9 and enforced by lint.
- **Shape B sentinel row (silent-upgrade + negative-constraint violation).** Row 0 fires HIGH-WARN-SILENT-EPISTEMIC-UPGRADE. v3.8 row 4 (UNSUPPORTED + negative_constraint_violation) also fires HIGH-WARN-NEGATIVE-CONSTRAINT-VIOLATION. The list returned to formatter contains both. Formatter REFUSE rules emit both annotations on the offending row in the output; the writer sees both blockers.
- **v3.8-only row (no silent upgrade).** `silent_upgrade_detected` is `false` or absent. Row 0 contributes nothing; v3.8 row 1-8 emits its single annotation as before. The return-shape change is a no-op for these rows (list of length 1).

#### 6.2.2 What does NOT co-occur with silent-upgrade

The list-based composition rule above is needed only for the `(silent_upgrade + negative_constraint)` pair. Two other v3.8 HIGH-WARN classes can in principle conflict, but in practice cannot reach a silent-upgrade row:

- **ANCHORLESS** (`anchor_kind == "none"`, `ref_retrieval_method == "not_attempted"`). Anchorless rows have no emitted-prose locator surface, so the v3.10 §5.3 inference cannot compute `emitted_epistemic_status` — these rows carry the §5.7.4 retrieval-short-circuit sentinel shape (Shape C, see below) with `silent_upgrade_detected == false`. Row 0 does not fire; v3.8 row 6 emits HIGH-WARN-CLAIM-AUDIT-ANCHORLESS as expected.
- **FABRICATED-REFERENCE** (`judgment == "RETRIEVAL_FAILED"`, `defect_stage == "retrieval_existence"`, `ref_retrieval_method == "not_found"`). Fabricated rows never reach the v3.10 emitted-status inference; the audit pipeline at §5.1 step C2.5 (new — see §5.1 update) gates retrieval failures before step C3 inference. The row carries Shape C sentinel; `silent_upgrade_detected == false`; row 0 does not fire; v3.8 row 5 emits HIGH-WARN-FABRICATED-REFERENCE.

#### 6.2.3 `_classify_silent_upgrade` return shape

The new helper emits one dict that conforms to the v3.8 per-row classification shape:

```python
def _classify_silent_upgrade(entry: dict) -> dict:
    return {
        "annotation": (
            "[HIGH-WARN-SILENT-EPISTEMIC-UPGRADE — claim "
            f"{entry['claim_id']} in manifest {entry['scoped_manifest_id']}: "
            f"source={entry['source_epistemic_status']}, "
            f"emitted={entry['emitted_epistemic_status']}]"
        ),
        "tier": "HIGH-WARN",
        "gate_refuse": True,
    }
```

The dict shape is byte-equivalent to v3.8 row return dicts. List composition is implemented at the `classify_claim_audit_result` level (§6.2.1), not at the helper level.

#### 6.2.4 INV-19 restated for the list-based composition

INV-19 (lint-enforced cross-field invariant on `claim_audit_result.schema.json`): every row matches exactly one of the four §5.7 row shapes (Shape A silent-upgrade sentinel, Shape B silent-upgrade-with-negative-constraint sentinel, Shape C retrieval-short-circuit, Shape D normal successful row). Shapes A and B are the two `silent_upgrade_detected == true` shapes; Shapes C and D are the two `silent_upgrade_detected == false` shapes (C when retrieval short-circuited at C2, D when the row reached C3 inference and the upgrade comparator did not fire). INV-19 governs schema-side row state; finalizer dispatch behavior is governed by §6.2.1's list-based composition.

### 6.3 Stage 6 histograms — two distinct keying schemes

The v3.8 Stage 6 histogram is keyed by `defect_stage` (the judge's defect taxonomy). The v3.10 silent-upgrade signal is **not** in the `defect_stage` enum (see §4.2 rationale), so the v3.8 histogram alone cannot report silent-upgrade counts without misrouting.

v3.10 introduces a parallel histogram keyed by **finalizer class** rather than `defect_stage`. Both histograms render in the Stage 6 AI Self-Reflection Report:

- The **`defect_stage` histogram** (existing v3.8) reports counts of each `defect_stage` value across `claim_audit_results[]` rows. v3.10 leaves this histogram unchanged in shape; rows where `silent_upgrade_detected == true` carry `defect_stage: null` (§5.7.2 sentinel tuple) and so contribute to the existing `null`-defect-stage bucket without distorting the v3.8 distribution.
- The **finalizer-class histogram** (new v3.10) reports counts of each HIGH-WARN class (and the existing LOW-WARN / MED-WARN classes) emitted by the finalizer matrix. This histogram is what tells the user how often each finalizer class fires, including the new `SILENT-EPISTEMIC-UPGRADE` class.

`scripts/claim_audit_finalizer.py:render_stage6_histograms` (plural, renamed from singular) emits both. The Stage 6 prompt's histogram-reading instructions extend to acknowledge the two histograms with distinct keying. Format is identical for both (one row per category, count column, descriptive caption); only the key column changes.

Why two histograms rather than one merged: the `defect_stage` taxonomy is structural to the judge's decision space and is reused by external tooling (calibration runner, FNR/FPR computation per `academic-pipeline/references/claim_audit_calibration_protocol.md`). Conflating finalizer classes with `defect_stage` values would either bloat the v3.8 enum or require a coercion step that loses information. The parallel-histogram design preserves both data shapes.

### 6.4 Vocabulary-doc backlink lint

`scripts/check_epistemic_status_vocabulary_backlink.py` (new lint) enforces three structural properties on the vocabulary documentation, replacing what an earlier spec draft incorrectly called an "identity lint" (the earlier framing required two byte-equivalent copies of the same table, contradicting the §3.4 single-source-of-truth invariant). The corrected lint verifies:

- **Backlink present.** `shared/references/epistemic_status_vocabulary.md` contains a markdown link to `deep-research/references/argumentation_reasoning_framework.md#epistemic-status-of-claims` (the canonical table's section anchor).
- **No duplicated table.** The vocabulary doc does NOT contain a second `| Status | Meaning | Appropriate Language |` table header. The lint runs a regex over the vocabulary doc's markdown source and rejects on any match.
- **Verbatim label string presence.** The vocabulary doc's prose mentions all five labels (`Established`, `Supported`, `Preliminary`, `Speculative`, `Contested`) verbatim and inside backticks, at least once each, so a reader searching for any label finds the canonical hit even without following the backlink.

Failure modes the lint catches: (a) backlink rot if the upstream anchor is renamed; (b) accidental table duplication by a future contributor; (c) typos in the label strings (e.g., `Established` mis-spelled as `Esablished`).

### 6.5 Schema-level cross-field invariant lint

`scripts/check_claim_audit_consistency.py` (existing v3.8 module) gains the v3.10 cross-field invariants. Each invariant is enforced both on stored passport files and on in-memory candidates produced by the pipeline (where applicable):

**Manifest schema invariants (claim_intent_manifest.schema.json):**
- **M-INV-5 (per-ref quoted_span polarity).** Within each `per_ref_assignments[]` entry: `quoted_span` non-null iff `confidence ∈ {"high", "medium"}`; null iff `confidence ∈ {"low_no_source", "low_no_match", "backfilled_low"}`. Per §4.1.
- **M-INV-6 (per_ref / planned_refs length parity).** `per_ref_assignments[]` length equals `planned_refs[]` length, with `ref_slug` matching at each index. Per §4.1.
- **M-INV-7 (aggregation correctness).** Claim-level `epistemic_status` matches the §5.2.6 weakest-source-wins reduction over `per_ref_assignments[].epistemic_status`. Claim-level `confidence` matches the minimum-confidence reduction over the §5.2.6 total order `high > medium > low_no_match > low_no_source > backfilled_low`. Empty `per_ref_assignments[]` requires `epistemic_status == "Speculative"` AND `confidence ∈ {"low_no_source", "backfilled_low"}` paired with the §4.1 M-INV-7 / M-INV-8 empty-array rule.
- **M-INV-8 (extraction_method coherence).** For non-empty `per_ref_assignments[]`: claim-level `extraction_method == "mixed_per_ref"` iff per-ref entries use ≥ 2 distinct extraction_methods; otherwise claim-level matches the homogeneous per-ref value. For empty `per_ref_assignments[]`: claim-level `extraction_method == "low_confidence_fallback"` paired with `confidence == "low_no_source"`, or `extraction_method == "backfill_default"` paired with `confidence == "backfilled_low"`. Per §4.1 expansion.

**Audit-result schema invariants (claim_audit_result.schema.json):**
- **INV-11 (existing v3.8, preserved unchanged).** `ref_retrieval_method == "not_attempted" ↔ anchor_kind == "none"`. v3.10 does not modify the v3.8 enum or its polarity. Shape A and Shape B sentinels preserve the row's actual retrieval method (`api` / `manual_pdf`); they do not synthesize a new method value. Paywall (`failed`) routes through Shape C.3, not Shape A/B, because v3.8 INV-10 requires `failed → RETRIEVAL_FAILED + inconclusive + not_applicable`, which is incompatible with Shape A/B's `AMBIGUOUS + completed + null` tuple per §5.7.2.
- **INV-19 (four-row-shape coherence).** Every `claim_audit_result` row matches exactly one of Shape A / Shape B / Shape C / Shape D per §5.7.2. Shape A (silent-upgrade only): `silent_upgrade_detected == true` + `judgment == "AMBIGUOUS"` + `defect_stage == null` + `judge_model == "silent_upgrade_check_v1"` + `ref_retrieval_method ∈ {"api", "manual_pdf"}` + rationale matches the Shape A template. Shape B (silent-upgrade + constraint violation): `silent_upgrade_detected == true` + `judgment == "UNSUPPORTED"` + `defect_stage == "negative_constraint_violation"` + `violated_constraint_id` populated + `judge_model == "silent_upgrade_check_v1+negative_constraint_judge_v3_8"` + rationale matches the Shape B two-segment template + `ref_retrieval_method ∈ {"api", "manual_pdf"}`. Shape C (retrieval-skipped): `silent_upgrade_detected == false` + `source_epistemic_status == "Speculative"` + `emitted_epistemic_status == "Speculative"` + the row carries an unchanged v3.8 RETRIEVAL_FAILED dispatch tuple (`judgment == "RETRIEVAL_FAILED"` + `audit_status == "inconclusive"` + `defect_stage == "not_applicable"`). The four §5.7.2 sub-cases C.1 / C.2 / C.3 / C.4 differ on `ref_retrieval_method` and `anchor_kind` values per §5.7.2; paywall (C.3) uses `ref_retrieval_method == "failed"`. Shape D (normal successful row, no silent upgrade and retrieval completed): `silent_upgrade_detected == false` + `source_epistemic_status` and `emitted_epistemic_status` reflect the per-(claim, ref) inference output (any of the five labels) + the row carries an unchanged v3.8 SUPPORTED / AMBIGUOUS / UNSUPPORTED dispatch tuple. Shape D is the dominant case — a successful audit where the writer's register matches or undershoots the source register. Other column combinations are lint failures. Lint test fixtures include one row exercising each of the four shapes (A / B / C / D) plus one v3.8-only `(SUPPORTED, null)` baseline.
- **INV-20 (Contested sidetrack respected — both sides).** `silent_upgrade_detected == true` is forbidden whenever `source_epistemic_status == "Contested"` OR `emitted_epistemic_status == "Contested"`. Lint rejects any matching row. This covers both polarities of the §3.3 sidetrack rule (the §3.2 `is_silent_upgrade` comparator short-circuits when either side is `Contested`).
- **INV-21 (negative-constraint judge always runs, with Shape C dispatch-preservation).** Every row whose underlying claim has any active negative constraints MUST receive negative-constraint judge evaluation, regardless of `silent_upgrade_detected`. Enforcement per shape, per §4.2 INV-21: Shape A / B / D rows surface the constraint result in the standard v3.8 dispatch path (Shape A appends `"| negative_constraint_judge: NOT_VIOLATED ..."`, Shape B uses the standard v3.8 negative_constraint_violation tuple, Shape D rows route through the v3.8 UNSUPPORTED branch on VIOLATED). Shape C (retrieval-skipped) rows preserve their v3.8 RETRIEVAL_FAILED tuple unchanged — the constraint judge result is appended to rationale as an attestation line only, never overwrites `judgment` / `defect_stage` / `ref_retrieval_method`. Per §5.7.3.

### 6.6 Path-filtered CI trigger

`.github/workflows/spec-consistency.yml` extends its path filter to trigger on:

**Schemas and references:**
- `shared/contracts/passport/claim_intent_manifest.schema.json`
- `shared/contracts/passport/claim_audit_result.schema.json`
- `shared/contracts/passport/constraint_violation.schema.json` (advisory-field only)
- `shared/contracts/passport/uncited_assertion.schema.json` (advisory-field only)
- `shared/references/epistemic_status_vocabulary.md`
- `deep-research/references/argumentation_reasoning_framework.md` (so the §6.4 backlink lint catches anchor rot)

**Implementation modules:**
- `scripts/epistemic_status_extractor.py`
- `scripts/check_silent_upgrade.py`
- `scripts/check_epistemic_status_vocabulary_backlink.py`
- `scripts/check_v3_10_silent_upgrade_refusal.py`
- `scripts/migrate_passport_to_v3_10_epistemic_status.py`

**Tests and fixtures:**
- `scripts/test_epistemic_status_schema.py`
- `scripts/test_silent_upgrade_check.py`
- `scripts/test_epistemic_status_extractor.py`
- `scripts/test_claim_audit_pipeline_v3_10.py`
- `scripts/test_e2e_epistemic_status.py`
- `scripts/test_migrate_passport_to_v3_10_epistemic_status.py`
- `tests/fixtures/v3_10_epistemic_status/**`
- `tests/fixtures/v3_10_e2e_epistemic_status/**`

**Agent files for prompt-level changes:**
- `academic-paper/agents/formatter_agent.md` (for rule 11)
- `deep-research/agents/synthesis_agent.md`
- `academic-paper/agents/draft_writer_agent.md`
- `deep-research/agents/report_compiler_agent.md` (the three writer agents gain the new `Epistemic Status Consumption (v3.10)` section)

Concurrency group uses the `${{ github.event_name }}` suffix to avoid push/PR group collisions (per repo CI discipline). The expanded test-path trigger list ensures test-only changes do not bypass the spec-consistency workflow (an earlier draft of this spec omitted the test paths, which would have let a buggy test land green).

---

## 7. Migration

`scripts/migrate_passport_to_v3_10_epistemic_status.py` handles pre-v3.10 passport upgrades.

### 7.1 In-place vs new-file migration

Following the v3.9.0 `migrate_literature_corpus_to_v3_9_0.py` convention, the migration is **new-file**: the script writes a new passport file at `<original>.v3.10.json` and never mutates the original in place. The original remains a v3.7.3 / v3.8.x artifact and may be re-migrated if defaults change in a future patch.

### 7.2 Backfill defaults

For every `claim_intent_manifest.claims[]` entry in the original passport that lacks `epistemic_status`:

- Claim-level `epistemic_status: "Speculative"` (the ordinal-ladder floor).
- Claim-level `epistemic_status_source.extraction_method: "backfill_default"`.
- Claim-level `epistemic_status_source.confidence: "backfilled_low"`.
- Claim-level `epistemic_status_source.extractor_run_at: <migration script run timestamp>`.
- `epistemic_status_source.per_ref_assignments[]`: one entry per `planned_refs[]` entry (preserving the M-INV-6 length-parity invariant). Each per-ref entry carries:
  - `ref_slug: <copied from planned_refs[i]>`
  - `epistemic_status: "Speculative"`
  - `extraction_method: "backfill_default"`
  - `confidence: "backfilled_low"`
  - `quoted_span: null`
  - `source_pointer: null`
- If `planned_refs[]` is empty for the claim (definitional or normative claim), `per_ref_assignments[]` is also empty, matching the M-INV-6 / M-INV-7 contract for the empty case.

For every `claim_audit_result` row in the original passport that lacks `source_epistemic_status` / `emitted_epistemic_status` / `silent_upgrade_detected`:

- `source_epistemic_status: "Speculative"` (matches the manifest backfill default).
- `emitted_epistemic_status: "Speculative"`.
- `silent_upgrade_detected: false` (no upgrade detected when both sides default to floor; the §5.7 sentinel tuple is not applied because the lint did not fire).

Rationale for `Speculative` floor: the conservative default ensures (a) backfilled rows never spuriously fire the silent-upgrade lint (since emitted == source), (b) writers who re-run the pipeline against a migrated passport are forced to re-extract real status assignments (Tier 1 / 2) rather than inheriting backfilled defaults silently. The `backfilled_low` confidence value is intentionally distinct from `low_no_source` and `low_no_match`: it carries the additional semantic "migrated, not extracted" so downstream consumers (display agents, reviewer agents) can recognize the provenance.

### 7.3 Migration test coverage

`scripts/test_migrate_passport_to_v3_10_epistemic_status.py` covers:

- T-M1: v3.7.3 passport (pre-v3.8 manifest absent) → migration is no-op for claim_audit_results, no manifest backfill needed.
- T-M2: v3.8 passport with 1 manifest, 3 claims, no audit rows → migration backfills 3 claims with floor default, audit rows section empty.
- T-M3: v3.8 passport with 1 manifest, 3 claims, 5 audit rows → migration backfills 3 claims AND 5 audit rows.
- T-M4: Already-migrated passport (`manifest_version == "2.0"`) → migration short-circuits with idempotency exit code 2 (per repo migration script convention).
- T-M5: Malformed passport (manifest version 0.5 or other unknown) → migration aborts with non-zero exit and clear error message.
- T-M6: Migration round-trip: v3.8 passport → migrate → load with v3.10 schema validator → all M-INV-5 / M-INV-6 / M-INV-7 / M-INV-8 / INV-19 / INV-20 / INV-21 invariants satisfied.

---

## 8. Test fixtures

The fixtures exercise the **specific upgrade cases** the issue body names, plus the deterministic-gate and judge-composition boundary cases.

### 8.1 Fixture layout

```
tests/fixtures/v3_10_epistemic_status/
├── fixtures.yaml                    # Manifest with case_id → expected outcome mapping
├── case_E1_preliminary_to_established/
│   ├── source.md                    # Synthetic source paper with H3-matching language
│   ├── manifest.json                # Pre-emission manifest with extractor output
│   ├── draft.md                     # Draft prose with Layer 5 (unqualified) emitted register
│   ├── expected_audit_result.json   # silent_upgrade_detected: true
│   └── expected_finalizer.json      # HIGH-WARN-SILENT-EPISTEMIC-UPGRADE
├── case_E2_speculative_to_established/
│   └── ...                          # we hypothesize → causes
├── case_E3_speculative_to_supported/
│   └── ...                          # hypothesis → evidence suggests
├── case_E4_no_upgrade/
│   └── ...                          # source=Supported, emitted=Supported → false
├── case_E5_contested_to_established/
│   └── ...                          # Contested source → no lint (sidetrack)
├── case_E6_tier3_fallback/
│   └── ...                          # No abstract, no PDF, default Speculative
├── case_E7_tier2_wins_over_tier1/
│   └── ...                          # Abstract says hypothesize, PDF says replicated
├── case_E8_inv20_violation/
│   └── ...                          # Contested + silent_upgrade=true (rejected)
├── case_E9_writer_self_check_pass/
│   └── ...                          # Writer follows §5.6 self-check
├── case_E10_writer_attribution_escape/
│   └── ...                          # "Smith et al. claim that X is" → Speculative
├── case_E11_multi_ref_weakest_wins/
│   └── ...                          # 3 planned_refs, statuses [Established, Preliminary, Supported]
│                                      → claim-level Preliminary (weakest wins)
└── case_E12_multi_ref_with_contested/
    └── ...                          # 2 refs: [Supported, Contested]
                                       → claim-level Supported (ordinal-min ignores Contested)
```

### 8.2 Specific upgrade cases (issue body)

The issue body names three failure modes that v3.10 must catch. Each maps to a fixture:

- **Case E1 — Preliminary → Established (in-vitro to clinical).** Source: "Preliminary findings in cell-line assays indicate compound X reduces protein Y expression in our test cohort." Draft: "Compound X is a validated inhibitor of protein Y." Expected: `source=Preliminary`, `emitted=Established`, `silent_upgrade_detected=true`, finalizer fires HIGH-WARN-SILENT-EPISTEMIC-UPGRADE.

- **Case E2 — Speculative → Established (association to cause).** Source: "We hypothesize that X may be associated with Y outcomes, based on the theoretical framework of Smith (2021)." Draft: "X causes Y." Expected: `source=Speculative`, `emitted=Established`, `silent_upgrade_detected=true`, fires HIGH-WARN.

- **Case E3 — Speculative → Supported (hypothesis to conclusion register).** Source: "Based on the theoretical framework of Smith (2021), we propose that X mediates Y." Draft: "Evidence suggests X mediates Y." Expected: `source=Speculative`, `emitted=Supported`, `silent_upgrade_detected=true`, fires HIGH-WARN. This is a 2-level jump on the ordinal ladder; the lint is strict ordinal `>` (Section 3.2), so any positive ordinal delta fires the gate regardless of magnitude.

### 8.3 Boundary cases

- **Case E4 — no upgrade (negative control).** Source: "Evidence suggests X is associated with Y." Draft: "Evidence suggests X is associated with Y." Expected: §5.3 Layer 4 inferential hedge (`Evidence suggests`) frames the sentence; the embedded `X is` does not promote register to `Established` per the I-V13 outer-frame-wins rule. `source=Supported`, `emitted=Supported`, `silent_upgrade_detected=false`, no finalizer fire. This case is the critical regression test for the inference-precedence fix; a buggy implementation that matched `X is` first would mis-classify `emitted=Established` and fire a false-positive silent upgrade.

- **Case E5 — Contested sidetrack.** Source: "While some studies find X reduces Y, others find no effect (Jones 2020 vs Smith 2022)." Draft: "X reduces Y." Expected: `source=Contested`, `emitted=Established`, `silent_upgrade_detected=false` (Contested skips ordinal). The row goes to the v3.8 judge for `synthesis_overclaim` evaluation; the deterministic gate does not fire.

- **Case E6 — Tier 3 fallback.** Corpus entry has no abstract, no local PDF. Expected: extractor emits `epistemic_status=Speculative`, `confidence=low_no_source`. Draft prose at "Evidence suggests" register → `emitted=Supported` → `silent_upgrade_detected=true`. The conservative floor forces writer adjustment.

- **Case E7 — Tier 2 wins over Tier 1.** Corpus entry has abstract: "We hypothesize that compound X reduces tumor growth." Corpus entry has source_pointer to a local PDF whose full text contains: "Replicated across three independent cohorts (Smith 2020, Jones 2021, Chen 2022)." Extractor reads PDF (Tier 2), assigns `Established`. Tier 1 abstract reading would have assigned `Speculative`; Tier 2 precedence per §5.2 overrides.

- **Case E8 — INV-20 violation.** Manually crafted row with `source_epistemic_status=Contested` AND `silent_upgrade_detected=true`. Expected: `check_claim_audit_consistency.py` lint rejects with INV-20 error. This case must not appear in production data; the fixture exists only to assert the lint catches it.

- **Case E9 — Writer self-check pass.** Source: "Single-site pilot study (n=18) indicates X correlates with Y." Draft (after self-check): "A single-site pilot study (n=18) indicates X correlates with Y, suggesting further replication is warranted." Expected: extractor assigns `Preliminary`; emitted register `Preliminary`; no upgrade. The self-check at §5.6 successfully prevented escalation.

- **Case E10 — Writer attribution escape.** Source: "We hypothesize X is the dominant mechanism." Draft: "Smith et al. (2024) claim that X is the dominant mechanism." Expected: §5.3 Layer 2 attribution frame (`Smith et al. ... claim that`) wins over the embedded Layer 5 unqualified copula (`X is`). `emitted=Speculative` (reported speech). `source=Speculative` (from extractor H2 hypothesis match per §5.5 reordering). `silent_upgrade_detected=false`. The attribution escape allows the writer to faithfully report a strong claim without endorsing it, and the outer-frame-wins rule is what makes the escape work.

- **Case E11 — Multi-ref weakest-source-wins.** Claim has three `planned_refs[]`: `ref_A` (Tier 2, status `Established`, replicated meta-analysis), `ref_B` (Tier 2, status `Preliminary`, single-site pilot), `ref_C` (Tier 1, status `Supported`, abstract-level inferential hedge). Expected `per_ref_assignments[]`: three entries matching the per-ref statuses. Claim-level `epistemic_status = Preliminary` (the ordinal minimum). Claim-level `confidence = medium` (the minimum confidence over per-ref tuples — Tier 1 medium is weaker than Tier 2 high). Claim-level `extraction_method = "mixed_per_ref"`. Draft prose at `Supported` register fires the silent-upgrade lint (Supported > Preliminary on the ordinal ladder).

- **Case E12 — Multi-ref with one Contested ref.** Claim has two `planned_refs[]`: `ref_D` (status `Supported`), `ref_E` (status `Contested`). Expected: claim-level `epistemic_status = Supported` (the ordinal-minimum over the ordinal-ladder subset; `Contested` does not enter the comparison per §5.2.6 last paragraph). Claim-level `extraction_method = "mixed_per_ref"`. The `Contested` per-ref entry remains visible in `per_ref_assignments[]` for reviewer inspection. Draft prose at `Supported` register does not fire silent upgrade.

### 8.4 End-to-end synthetic-paper fixture

`tests/fixtures/v3_10_e2e_epistemic_status/` carries a 5-claim synthetic paper exercising the full pipeline. Mirrors the v3.8 5-citation end-to-end fixture pattern (`tests/fixtures/v3_8_e2e_claim_audit/`).

- 5 claims in the manifest, mixing all 5 statuses (1 Established, 1 Supported, 1 Preliminary, 1 Speculative, 1 Contested).
- 3 of the 5 claims have draft prose at the correct register (no upgrade).
- 1 claim has emitted register one level above source (silent upgrade detected, HIGH-WARN-SILENT-EPISTEMIC-UPGRADE).
- 1 claim has emitted register two levels above source (silent upgrade detected, same HIGH-WARN class; finalizer does not distinguish 1-level vs N-level upgrade).
- Expected formatter output: REFUSE with two rule-11 annotations.
- Expected Stage 6 histogram: row for HIGH-WARN-SILENT-EPISTEMIC-UPGRADE = 2.

---

## 9. INVARIANTS

Spec-level invariants enforced at lint, test, and runtime.

**I-V1 — Closed enum.** `epistemic_status` field values are exactly the set `{Established, Supported, Preliminary, Speculative, Contested}`. No alias, no case variant. Enforced by JSON Schema enum + pattern lint.

**I-V2 — Ordinal monotonicity.** The strict ordinal `>` operates on the 4-tier subset `{Established=4, Supported=3, Preliminary=2, Speculative=1}`. `Contested` is not on the ladder. The ordinal values are constants; future revisions cannot reorder without bumping schema version and providing a migration script.

**I-V3 — Contested sidetrack on both sides.** `Contested` on either `source_epistemic_status` OR `emitted_epistemic_status` skips the silent-upgrade lint per §3.3. INV-20 forbids `silent_upgrade_detected == true` whenever either side is `Contested` (covers both polarities of the §3.2 `is_silent_upgrade` short-circuit).

**I-V4 — Producer-before-consumer ordering.** §5.1 step ordering is binding: the extractor runs at Stage 4a (steps A1-A3) before the writer runs at Stage 4b (steps B1-B2); the silent-upgrade lint runs at step C4 before the `synthesis_overclaim` judge runs at step C5. Steps cannot be reordered without revising this spec.

**I-V5 — `synthesis_overclaim` judge never runs on silent-upgrade rows.** §5.4 point 3: a row with `silent_upgrade_detected=true` skips the `synthesis_overclaim` judge dispatch only. The `negative_constraint` judge still runs on every row (§5.4 point 4, §5.7.3, INV-21). The two HIGH-WARN classes that the `synthesis_overclaim` judge produces (SILENT-EPISTEMIC-UPGRADE via deterministic gate vs CLAIM-NOT-SUPPORTED via judge verdict) are mutually exclusive at the row level. Enforced by the §6.2.1 judgment-independent pre-check (which short-circuits the row before the v3.8 UNSUPPORTED branch can route it through CLAIM-NOT-SUPPORTED) and by the test fixture E1 + E2 cases.

**I-V6 — Single-source-of-truth on the 5-tier table.** §3.4: the 5-tier label-meaning-language table is defined in `deep-research/references/argumentation_reasoning_framework.md` §Epistemic Status of Claims. The new vocabulary doc at `shared/references/epistemic_status_vocabulary.md` backlinks to that table; it does not duplicate the table content. The §6.4 backlink lint enforces structural properties (backlink present, no duplicated table, verbatim label string presence) rather than byte-equivalence over duplicated content.

**I-V7 — No LLM at extraction time.** §5.2 and §5.3 (the extractor and the emitted-status inference) use only deterministic regex / substring patterns and pdftotext / abstract reading. §5.5 source-reading heuristics and §5.2.6 aggregation are purely arithmetic. No LLM judge runs during status assignment or silent-upgrade detection. This invariant is the load-bearing property that distinguishes the v3.10 deterministic gate from the v3.8 LLM-mediated judge.

**I-V8 — Pattern catalogue is additive within patch releases.** §5.3 emitted-status inference layers and §5.5 source-reading heuristics: new patterns may be added in a patch release without bumping schema version, because the schema enum values themselves do not change. However, adding a pattern can change which layer a given sentence matches first, so existing fixture-expected outputs (`source_epistemic_status`, `emitted_epistemic_status`) may shift on previously-classified text. Patch releases that add patterns MUST review the v3.10 fixture set and update expected outputs where the new pattern matches earlier than the previous catalogue allowed. Pattern **removal** requires a schema version bump and a migration script (because it can silently downgrade classifications on existing fixtures without an obvious failure mode).

**I-V9 — Three-tier extractor fallback is exhaustive.** §5.2: every (claim, ref) pair resolves to exactly one of Tier 1 (abstract_text), Tier 2 (pdftotext_full_text), or Tier 3 (low_confidence_fallback) per the §5.2.5 deterministic precedence rule. No "unresolved" per-ref state is permitted; the §5.2.6 aggregation guarantees claim-level resolution.

**I-V10 — Quoted-span polarity (M-INV-5).** Within each `per_ref_assignments[]` entry: `quoted_span` non-null iff `confidence ∈ {high, medium}`. Enforced by `check_claim_audit_consistency.py` and tested in T-M2 / T-M3 migration cases.

**I-V11 — Backfill default forces re-extraction.** §7.2: the conservative `Speculative` + `backfilled_low` default is deliberately weakest, so any writer running the pipeline against a migrated passport must re-extract real status assignments. The `backfilled_low` confidence value is distinct from `low_no_source` and `low_no_match` so downstream consumers can recognize the migration provenance.

**I-V12 — Writer cannot edit `epistemic_status` directly.** §5.6 point 5: the writer prompt forbids manual override of extractor-assigned status. Override paths are (a) source correction + upstream extractor re-run, (b) low-confidence advisory escalation to user. The writer is the *consumer* of `epistemic_status`, not its *producer*; production happens at Stage 4a per §5.1.

**I-V13 — Outer-frame-wins emitted inference.** §5.3.1 layer evaluation order is binding: Layer 1 (contestation) → Layer 2 (attribution) → Layer 3 (hypothesis) → Layer 4 (hedge frame) → Layer 5 (unqualified assertion fallback). The first layer that matches in a sentence wins. This invariant is what prevents false-positive silent-upgrade fires on standard academic prose ("Evidence suggests X is...", "Smith et al. claim that X is..."). Replacing this rule with "strongest assertion wins" or any equivalent re-prioritization would break the writer-side experience.

**I-V14 — Multi-ref weakest-source-wins aggregation.** §5.2.6: claim-level `epistemic_status` is the ordinal-minimum of `per_ref_assignments[].epistemic_status` (with the Contested handling carved out per §5.2.6 last paragraph). M-INV-7 enforces this at lint time. Replacing weakest-source-wins with strongest, modal, or vote semantics requires a schema version bump.

**I-V15 — Sentinel-tuple integrity.** §5.7.2: silent-upgrade rows skipping the `synthesis_overclaim` judge carry the canonical sentinel tuple verbatim in their judge-required columns. INV-19 enforces this at lint time. The tuple values cannot vary across runs (`judgment == "AMBIGUOUS"`, `audit_status == "completed"`, `defect_stage == null`, etc.); the only variable part is the rationale-template interpolation and the lint timestamp.

---

## 10. Acceptance criteria

Numbered for ship-readiness verification. Each item is testable.

**A1 — Schema migration shipped.** `shared/contracts/passport/claim_intent_manifest.schema.json` and `claim_audit_result.schema.json` carry the v3.10 fields per §4.1 and §4.2. `manifest_version` bumps to `"2.0"`. The optional advisory fields land in `constraint_violation.schema.json` and `uncited_assertion.schema.json` per §4.3 and §4.4.

**A2 — Reference doc shipped.** `shared/references/epistemic_status_vocabulary.md` exists, carries the schema-level vocabulary contract, and cites `deep-research/references/argumentation_reasoning_framework.md` as the upstream label-and-meaning source. The §6.4 backlink lint passes (backlink to canonical table present; no duplicated table; verbatim label strings present in prose).

**A3 — Extractor module shipped.** `scripts/epistemic_status_extractor.py` implements the three-tier fallback (§5.2) and the H1-H5 heuristic catalogue (§5.5). Unit tests `scripts/test_epistemic_status_extractor.py` pass with at least one fixture per tier and per heuristic.

**A4 — Silent-upgrade lint shipped.** `scripts/check_silent_upgrade.py` implements the strict ordinal `>` comparison per §3.2 and the Contested sidetrack rule per §3.3. Unit tests `scripts/test_silent_upgrade_check.py` cover the full 4×4 = 16 ordered pairs over the ordinal ladder, partitioned as: **6 upgrade pairs** (Speculative→Preliminary, Speculative→Supported, Speculative→Established, Preliminary→Supported, Preliminary→Established, Supported→Established) asserting `silent_upgrade_detected == true`; **4 self-pairs** (each ordinal value paired with itself) asserting `false`; **6 downgrade pairs** asserting `false`. Plus 9 unique Contested-involving pairs covering both polarities of INV-20: `(Contested, Established)`, `(Contested, Supported)`, `(Contested, Preliminary)`, `(Contested, Speculative)`, `(Established, Contested)`, `(Supported, Contested)`, `(Preliminary, Contested)`, `(Speculative, Contested)`, `(Contested, Contested)`; all 9 assert `silent_upgrade_detected == false`. Total: 25 unique ordered-pair test cases. The full coverage prevents a regression where a broken comparator passes only the upgrade-shaped cases.

**A5 — Pipeline integration shipped.** The pipeline runs the upstream extractor at Stage 4a (steps A1-A3 per §5.1), the writer at Stage 4b (B1-B2), and the silent-upgrade lint inside `scripts/claim_audit_pipeline.py` at the Stage 4→5 boundary (C3-C4). Rows with `silent_upgrade_detected=true` skip the `synthesis_overclaim` judge per I-V5; the `negative_constraint` judge still runs on every row per INV-21.

**A6 — Finalizer pre-check extension shipped.** `scripts/claim_audit_finalizer.py:classify_claim_audit_result` carries the judgment-independent pre-check at function entry per §6.2.1 and the `_classify_silent_upgrade` helper per §6.2.3. The v3.8 judgment-dispatch logic below the pre-check is byte-equivalent (no row reordering, no enum modification on `judgment`/`defect_stage`). Stage 6 histograms extend per §6.3 (two parallel histograms keyed by `defect_stage` and finalizer-class respectively).

**A7 — Formatter REFUSE rule 11 shipped.** `academic-paper/agents/formatter_agent.md` includes rule 11 per §6.1. `scripts/check_v3_10_silent_upgrade_refusal.py` lint passes.

**A8 — Writer agent updates shipped.** Each of `synthesis_agent`, `draft_writer_agent`, `report_compiler_agent` carries the new `## Epistemic Status Consumption (v3.10)` section per deliverable 13 in §1. Section content includes the writer self-check protocol per §5.6, and the section explicitly frames the writer as the consumer (not producer) of `epistemic_status`.

**A9 — Migration script shipped.** `scripts/migrate_passport_to_v3_10_epistemic_status.py` handles v3.7.3 / v3.8 / v3.8.x / v3.9.x passports per §7. T-M1 through T-M6 tests pass.

**A10 — Test fixtures shipped.** All 12 cases (E1-E12) in §8.1 land in `tests/fixtures/v3_10_epistemic_status/`. The end-to-end fixture at §8.4 lands. Test assertions use **schema-aware deep-equal comparison** after normalizing the two timestamp fields (`extractor_run_at`, `judge_run_at`) to a canonical frozen ISO-8601 value (e.g., `"2026-05-21T00:00:00Z"` — a valid `date-time` per JSON Schema, so the normalized fixture also passes schema validation). Tests inject the frozen clock via `monkeypatch._now` or equivalent so the extractor / migration script emit the canonical timestamp during fixture execution. Byte-equivalence is required on all non-timestamp fields after this normalization; the normalization itself preserves ISO-8601 conformance.

**A11 — CI wiring.** `.github/workflows/spec-consistency.yml` triggers on the path filter in §6.6. The concurrency group includes the `${{ github.event_name }}` suffix per repo CI discipline.

**A12 — Cross-schema invariants enforced.** `scripts/check_claim_audit_consistency.py` enforces all v3.10 cross-field invariants per §6.5: M-INV-5, M-INV-6, M-INV-7, M-INV-8 on the manifest schema; INV-11 (preserved unchanged from v3.8), INV-19, INV-20, INV-21 on the audit-result schema. Test fixtures exercise both satisfied and violated cases for each invariant. INV-19 fixtures cover all four §5.7.2 row shapes (one Shape A row, one Shape B row, one Shape C row, one Shape D row) plus one v3.8-only baseline row.

**A13 — Vocabulary backlink lint passes.** `scripts/check_epistemic_status_vocabulary_backlink.py` is wired into CI and passes against the v3.10 ship commit per §6.4 (backlink to canonical table present, no duplicated table in the vocabulary doc, verbatim label strings present in prose).

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

**A15 — Umbrella shared-contract claim verified.** The §3.6 `verification_gate` contract table is the source of truth for the shared interface across #182 / #183 / #184. **Conditional on the companion lite specs landing**: when `docs/design/2026-05-21-v3.10-182-promote-citation-gate-spec.md` and `docs/design/2026-05-21-v3.10-184-extend-eval-harness-spec.md` exist in the repo, both cite this section by path AND their respective contract columns match the §3.6 table verbatim. If either lite spec is still in draft at the v3.10 ship cutoff, A15 reduces to "the §3.6 contract table is internally consistent with §4-§6 implementation surface" and the cross-spec consistency check is deferred to the lite specs' own ship gate.

---

## 11. Open questions deferred to implementation

These were considered during spec drafting but deferred to the implementation phase. None block ship of the spec.

**OQ-1 — pdftotext page cap and helper extraction.** §5.2.3 Tier 2 caps full-text extraction at 50 pages (a guess; the actual cap should be tuned in implementation based on observed paper lengths in the corpus). The implementation is also free to either inline the v3.9.4 pdftotext subprocess pattern (the path of least resistance, matching `bootstrap_timeline_yaml.py:_pdftotext_first_line`) or extract a shared helper if reuse warrants. Both are runtime / refactoring concerns, not schema concerns, so neither bumps schema version.

**OQ-2 — quoted_span max length.** §4.1 sets `maxLength: 1000` on `quoted_span`. Long quotes from full-text papers may exceed this. Implementation may relax to 2000 or add a truncation indicator (`...`) at the end of truncated quotes. Decision deferred.

**OQ-3 — Reviewer agent integration.** The `epistemic_status` field is visible to reviewer agents (since they read the manifest), but no reviewer prompt currently references it. Whether to add a reviewer-side prompt section (`## Reading Epistemic Status (v3.10)`) is deferred to the v3.10 implementation phase, after the silent-upgrade lint's false-positive rate is measured empirically.

**OQ-4 — Stage 6 histogram aggregation across runs.** The histograms per §6.3 are per-run. Cross-run aggregation (e.g., "this user's last 10 runs have a rising trend in silent-upgrade-detected counts") is a separate concern. Deferred.

---

## 12. References

- Issue #183: https://github.com/Imbad0202/academic-research-skills/issues/183
- Umbrella #198: https://github.com/Imbad0202/academic-research-skills/issues/198
- Companion lite spec #182: `docs/design/2026-05-21-v3.10-182-promote-citation-gate-spec.md` (to be drafted next)
- Companion lite spec #184: `docs/design/2026-05-21-v3.10-184-extend-eval-harness-spec.md` (to be drafted after #182)
- v3.8 source-of-truth: `docs/design/2026-05-15-issue-103-claim-alignment-audit-spec.md`
- Reasoning-framework reference (5-tier upstream definition): `deep-research/references/argumentation_reasoning_framework.md` §Epistemic Status of Claims
- Protected-hedging reference (compression vs schema-status distinction): `shared/references/protected_hedging_phrases.md`
- v3.7.3 anchor protocol (locator side of L3): `docs/design/2026-05-12-ars-v3.7.3-claim-faithfulness-and-contaminated-source-spec.md`
- v3.9.4 first-party verification (pdftotext infra reused by Tier 2): `shared/contracts/passport/citation_provenance.schema.json`
- External motivation: Ghareeb et al. 2026 (specialized literature agent versus generic frontier model on drug-discovery proposal hallucination rate). Cited only as a reference number; production decisions wait for local gold-set numbers per #184.
