epistemic_status Field + Silent-Upgrade DetectionDate: 2026-05-21
Issue: #183
Umbrella: #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:
epistemic_status enum field on claim-bearing passport schemas (no current schema has it).silent_upgrade_check lint that compares extractor-assigned status against summarizer prose register using strict ordinal > comparison.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.
Numbered to match implementation surfaces. All paths relative to repo root.
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.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.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.shared/contracts/passport/uncited_assertion.schema.json — row gains optional claim_epistemic_status advisory field. Same version-bump policy as constraint_violation.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).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.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).scripts/claim_audit_pipeline.py — extended with two distinct integration points reflecting the §5.1 producer-before-consumer lifecycle: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.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.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.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.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.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.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.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.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..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.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.Carried from the umbrella issue gap analysis and the Step C Q2 vocabulary lock. Read in conjunction with the umbrella out-of-scope list.
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.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.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.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.
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:
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).
Contested is a sidetrackContested 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).
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:
Contested sidetrack rule.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.
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.
verification_gate shared contract surfaceThis 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.
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.
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"].
"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):
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"}.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).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.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.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"].
"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.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.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):
claim_audit_result row matches exactly one of the four §5.7.2 row shapes: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.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.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.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.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.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: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."| 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.constraint_violation.schema.json (advisory)Add optional row field:
"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).
uncited_assertion.schema.json (advisory)Same optional advisory field as §4.3:
"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.
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.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:
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.
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.
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).
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.
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.
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.
abstract_textWhen 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>.
pdftotext_full_textWhen 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>.
low_confidence_fallbackWhen 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.
Per ref, the extractor follows this deterministic precedence:
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.literature_corpus_entry.abstract is non-empty, run Tier 1 against the abstract. Tier 1's result is the per-ref tuple.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.
The per-ref tuple array is reduced to a claim-level epistemic_status by the weakest-source-wins rule:
epistemic_status per §3.2.Established / Supported / Preliminary / Speculative), the claim-level epistemic_status is the value with the lowest ordinal.epistemic_status: "Contested" AND no per-ref tuple is on the ordinal ladder, the claim-level epistemic_status is Contested (the sidetrack).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):
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.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).
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).
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:
may, might, could) → emitted register = Preliminary (the writer signals reduced confidence on a claim).preliminary findings, pilot study, single-site study, in this exploratory cohort) → emitted register = Preliminary.Evidence suggests, results indicate, findings are consistent with) → emitted register = Supported (the writer attributes the claim to evidence, not asserts it directly).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.
The previous spec draft said "strongest unqualified assertion wins" when multiple patterns match in the same sentence. That rule has two pathological cases:
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.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.
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.
synthesis_overclaim judgeThe v3.8 claim_audit_result.defect_stage == "synthesis_overclaim" defect class continues to operate as the LLM-mediated downstream fallback. The two layers compose:
#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.
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).
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.
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.
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.
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:
claim_text (from the manifest) and the source text (abstract or full text per §5.2 Tier policy) into whitespace-split lowercase tokens.a / an / the / of / in / on / and / or / for / to / with / is / are / was / were).Speculative + low_no_match) without running H1-H5.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.
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.
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.Preliminary claim must not use "X is", "X causes", "X demonstrates"; instead, "Preliminary findings indicate X" or "in this exploratory cohort, X".Smith et al. claim that X is.... The §5.3 inference treats reported speech as Speculative (attributed assertion, not endorsed assertion).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).
The §5.1 step C3 candidate-row construction and the §5.1 step C5 judge-skipping rule together require two formal contracts:
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.
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.
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. |
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. |
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.
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.
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_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.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.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).
academic-paper/agents/formatter_agent.md Cite-Time Provenance Hard Gate REFUSE list extends from v3.8's 10 rules to 12:
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}].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).
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.
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:
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:
_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.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).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:
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.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._classify_silent_upgrade return shapeThe new helper emits one dict that conforms to the v3.8 per-row classification shape:
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.
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.
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:
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.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.
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:
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).| Status | Meaning | Appropriate Language | table header. The lint runs a regex over the vocabulary doc's markdown source and rejects on any match.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).
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.
.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).
scripts/migrate_passport_to_v3_10_epistemic_status.py handles pre-v3.10 passport upgrades.
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.
For every claim_intent_manifest.claims[] entry in the original passport that lacks epistemic_status:
epistemic_status: "Speculative" (the ordinal-ladder floor).epistemic_status_source.extraction_method: "backfill_default".epistemic_status_source.confidence: "backfilled_low".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: nullsource_pointer: nullplanned_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.
scripts/test_migrate_passport_to_v3_10_epistemic_status.py covers:
manifest_version == "2.0") → migration short-circuits with idempotency exit code 2 (per repo migration script convention).The fixtures exercise the specific upgrade cases the issue body names, plus the deterministic-gate and judge-composition boundary cases.
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)
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.
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.
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/).
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.
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.
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.
docs/design/2026-05-21-v3.10-182-promote-citation-gate-spec.md (to be drafted next)docs/design/2026-05-21-v3.10-184-extend-eval-harness-spec.md (to be drafted after #182)docs/design/2026-05-15-issue-103-claim-alignment-audit-spec.mddeep-research/references/argumentation_reasoning_framework.md §Epistemic Status of Claimsshared/references/protected_hedging_phrases.mddocs/design/2026-05-12-ars-v3.7.3-claim-faithfulness-and-contaminated-source-spec.mdshared/contracts/passport/citation_provenance.schema.json