# ARS v3.9.4 — Temporal Verification Layer Spec

**Status:** Design draft (pre-codex-review)
**Date:** 2026-05-18
**Target release:** v3.9.4
**Scope:** Add a deterministic temporal-integrity verification layer to ARS, addressing the LLM structural failure that prose reads "fluent" but mis-orders dates, attributes future-versioned documents to past cases, hallucinates comparator versions, asserts implicit causal inversions, and uses time-bomb deictic language. **All v3.9.4 additions are advisory-only.** Terminal gate behavior is preserved from v3.9.0 (R-L3-2-A firm rule: contamination/temporal signals never block emission on their own). Hard-block policies, full reviewer-dimension integration, full PDF-cover audit, and causal-relation manifests are deferred to v3.10 as part of the active conductor track (#134).
**Originating issue:** GitHub #135 — "LLM lacks temporal/chronological reasoning; pipeline needs deterministic timeline verification"
**Precedent docs:**

- `2026-05-12-ars-v3.7.3-claim-faithfulness-and-contaminated-source-spec.md` (§3.2 L3-2 contamination signals — single-vector pattern)
- `2026-05-17-ars-v3.9.0-cross-index-triangulation-measurement-spec.md` (advisory-only extension precedent, sidecar-vs-extension split discipline)

**External motivation:** Issue #135 documents the structural failure: LLM next-token objectives reward linguistic plausibility and are systematically blind to deterministic factual classes (citation, temporal, arithmetic, version, numerical). Temporal integrity is the second class addressed in ARS — citation is the first, via v3.7.3 / v3.9.0 contamination_signals + v3.8 claim_audit_result. This spec adopts the same advisory-measurement-layer-now / policy-layer-later split.

---

## 1. Background

### 1.1 What v3.7.3 / v3.8 / v3.9.0 already cover

| Layer | Mechanism | Catches |
|---|---|---|
| v3.7.3 contamination_signals | preprint heuristic + Semantic Scholar lookup | Recently-published preprint references + fabricated reference detection (single-index) |
| v3.8 claim_audit_result | Cite-time provenance finalizer at Phase 4 → 5 boundary | claim ↔ ref alignment, anchor faithfulness, synthesis overclaim |
| v3.9.0 cross-index triangulation | OpenAlex + Crossref lookups | False-positive reduction on fabricated-reference detection (4-tier advisory matrix) |

None of these address temporal validity. v3.8 `claim_audit_result.defect_stage` enum has 8 values: `retrieval_existence`, `metadata`, `source_description`, `citation_anchor`, `synthesis_overclaim`, `negative_constraint_violation`, `not_applicable`, `null`. **None is temporal.** v3.8 `claim_intent_manifest.intended_evidence_kind` enum has 4 values: `empirical`, `theoretical`, `definitional`, `normative`. **None is temporal.**

### 1.2 The five failure modes (from #135)

Synthesised from observed cases:

1. **Future-as-past.** A document dated month X of year Y is described as already reporting an event scheduled for month X+N of year Y. Physically impossible. Reads as fluent prose.
2. **Version-as-evidence-for-past-cases.** A standard/handbook/specification published in year Y+1 is cited as the framework governing cases occurring in years Y-2 through Y. The actually-governing earlier version is silently overwritten by the model's tendency to cite the most recent known version.
3. **Comparator hallucination across versions.** When discussing changes between document versions ("the newer edition adds X not present in the [prior] edition"), the model hallucinates the prior version's year. The hallucinated year is confabulated from contextual proximity to another similarly-dated source. The cited "prior version" often does not exist.
4. **Causal inversion via implicit temporal claim.** The research output narrates event A as a consequence of event B, but the dates the output itself cites place A before B. The model reads this as smooth prose because "A happened because of B" is a syntactically common pattern.
5. **Time-bomb references.** The model writes "the most recent edition (the YYYY edition)" or "the new framework launched last year" or "currently holds [X status]." These work at writing time but become false post-publication.

### 1.3 Why current pipeline does not catch this

Phase 4 `report_compiler_agent` writes prose by attending to source documents but does not maintain an explicit timeline. Phase 5 reviewers (editor / DA / ethics) read the output prose and judge plausibility, fluency, argument structure — none of which surface temporal errors unless date arithmetic is obvious to a reasoning-effort inline chain (it usually is not). Phase 2 `source_verification_agent` does not currently extract per-source date metadata into a timeline that downstream agents can verify against.

The author chain (LLM-prose-writer) and the reviewer chain (LLM-prose-reader) share next-token-likelihood training objectives and are systematically blind to the same class of error — adding more reviewer rounds does not converge on temporal correctness. See #135 for empirical signal.

---

## 2. Goal & Non-Goals

### 2.1 Goal

Add a deterministic temporal-integrity verification layer that:

1. Materialises per-source temporal facts (issued date, effective-date range, supersession chain, known versions) into a **sidecar artifact** `phase2_investigation/timeline.yaml` — **never** mutates `literature_corpus_entry.schema.json` (which is `additionalProperties: false` adapter-owned).
2. Verifies prose-level date claims at Phase 4 → Phase 5 boundary via a deterministic script (`scripts/temporal_integrity_audit.py`), emitting `phase4_composition/temporal_audit_results.yaml` + a human-readable `phase4_composition/temporal_audit.md`. **No `defect_stage` extension to v3.8 `claim_audit_result.schema.json`** — temporal findings ride in their own aggregate (mirrors why v3.8 split `claim_drifts` / `uncited_assertions` / `constraint_violations` into sibling aggregates).
3. Records first-party citation provenance for date-bearing references via a separate sidecar `phase2_investigation/citation_provenance.yaml` — so M2's deterministic verifier does not become "precise arithmetic over LLM-confabulated dates."
4. Adds non-negotiable temporal-integrity instructions to `report_compiler_agent` (prevention, not verification — prompt-level reduction of error rate).
5. Requires `precision` everywhere a date appears in the timeline (`day` / `month` / `year` / `interval` / `unknown`) to prevent fake precision.
6. Preserves v3.7.3 R-L3-2-A firm rule unchanged: **all temporal findings are advisory; emission is never blocked by temporal signals alone in v3.9.4.**

### 2.2 Non-Goals (deferred to v3.10)

The following are explicitly out of v3.9.4 scope:

- **No hard-block tier.** No `TEMPORAL-HIGH-BLOCK`. The formatter refusal list (v3.7.3 + v3.9.0) is unchanged. Block-eligible classes are *defined* in §6 for forward-compat with strict policy, but terminal behavior remains advisory.
- **No Phase 5 reviewer-dimension extension.** `editor_in_chief_agent` and `devils_advocate_agent` are not modified. The audit artifact is *available* for them to consume in v3.10, but no new review dimension is added in v3.9.4. Reasoning: M4 reviewer changes belong with #134 conductor's structured task-envelope return contract, which will frame the reviewer's relationship to the temporal audit artifact.
- **No full PDF cover-page audit for citation provenance.** M6 in v3.9.4 ships **Crossref `issued` date lookup + pdftotext front-page first-line scan only**. Full bibliographic cross-check (title, authors, venue) against PDF cover is deferred to v3.10 — that requires VLM integration or expanded pdftotext parsing.
- **No temporal-relation manifest.** "A enabled B" / "X superseded Y" relation tuples are not emitted as a structured artifact in v3.9.4. M2 catches *explicit* dated causal claims; *implicit* causal claims without trigger phrases are out of scope. M8 deferred.
- **No `version_catalog_completeness` semantics.** The flag `version_catalog_completeness: exhaustive | partial | unknown` is present in the timeline schema for forward-compat, but v3.9.4 does not act on `exhaustive` — Mode 3 phantom-vs-uncataloged disambiguation is deferred to v3.10. v3.9.4 emits `TEMPORAL-COMPARATOR-UNMATERIALIZED` whenever a comparator version is referenced but absent from the timeline, regardless of completeness — this is *claim-support* advisory, not phantom-existence judgment.
- **No automatic backfill of `effective_date_range`.** Crossref API lookups can populate `published_date` (publication date is the publisher's record), but **effective_date_range is a different concept** (the period during which a handbook/standard governs cases) and cannot be inferred from publication metadata. Bootstrap script populates `published_date` only. (v3.10 will extend to OpenAlex `publication_date` once the API protocol reference doc is ported from v3.9.0 conventions.)
- **No mutation of `literature_corpus[]`.** Legacy corpus entries stay legacy. Verifier falls back to year-only interval semantics (`year: 2024` → `2024-01-01..2024-12-31`) when no `timeline.yaml` entry covers a citation_key.

### 2.3 v3.10 forward scope (acceptance criteria)

To keep v3.9.4 a stable measurement baseline without leaving the policy story open-ended:

| Item | v3.10 acceptance criterion |
|---|---|
| M4 reviewer-dimension integration | `editor_in_chief_agent` + `devils_advocate_agent` gain `## Temporal Integrity Review (v3.10)` section consuming `temporal_audit_results.yaml`, focused on implicit causal relations and missed time-bombs (not re-discovery). |
| M5 full version preservation | `timeline_extraction_agent` outputs full `version_family_id` chain for institutional documents with ≥ 2 versions (proactive discovery, not user-declared-only); downstream agents join via version_family_id. `bibliography_agent` remains unmodified. |
| M6 full PDF cover audit | First-party verification extends from Crossref-issued-date lookup to full title/author/venue cross-check against PDF cover via pdftotext / VLM. |
| M8 temporal-relation manifest | Phase 4 emits `phase4_composition/temporal_relations.yaml` with sentence-id-bound relation tuples (`enabled` / `superseded` / `governed` / `responded_to`). Catches implicit-causality Mode 4 not currently catchable. |
| CC5 version_catalog_completeness semantics | When `version_catalog_completeness: exhaustive` AND comparator absent, escalate from `TEMPORAL-COMPARATOR-UNMATERIALIZED` (claim-unsupported) to `TEMPORAL-COMPARATOR-PHANTOM` (claim-fabricated). |
| Hard-block policy | `triangulation_policy` field gains `strict_temporal` enum value. When set, Mode 1 (future-as-past disjoint-interval) and Mode 2 (version-as-evidence with disjoint effective range) promoted to `HIGH-BLOCK`. R-L3-2-A amended to explicit user-opt-in policy clause. |
| R-L3-2-A reword | From "Contamination signals never block emission on their own" to "By default, contamination AND temporal signals never block emission on their own; user-enabled strict policies may promote specified signals to terminal blockers." |

---

## 3. Mechanisms

This section specifies the six mechanisms (M1, M2, M3, M6-minimal, M7-minimal, plus M5-stub) and the seven cross-cutting decisions (CC1-CC4, CC6, CC7; CC5 deferred). Each mechanism specifies inputs, outputs, file locations, and the load-bearing invariants.

### 3.1 M1 — `timeline_extraction_agent` (Phase 2 sibling)

**Role.** New sibling agent to `source_verification_agent` and `bibliography_agent`, both already pinned to Phase 2 by v3.9.2 phase-boundary discipline. Its sole deliverables are `phase2_investigation/timeline.yaml` (temporal facts; this section) and `phase2_investigation/citation_provenance.yaml` (first-party citation verification; §3.4).

**Phase-boundary.** Same Phase 2 binding pattern as the existing v3.9.2 agents. `timeline_extraction_agent.md` carries the canonical Phase 2 block (copied verbatim from `source_verification_agent.md` lines 12-26, only the phase/deliverable text adapted):

```
## Phase Boundary (v3.9.4)

You are a single-phase agent assigned to **Phase 2 (Investigation)** — same phase as
`bibliography_agent` and `source_verification_agent`. Your sole deliverables are:

- `phase2_investigation/timeline.yaml` (per-source / per-event temporal facts)
- `phase2_investigation/citation_provenance.yaml` (per-citation first-party
  verification results — Crossref `issued` date lookup + pdftotext cover scan)

You MUST NOT:

- WRITE files in `phase{M}_*/` directories where M ≠ 2 (no inflate into Phase 3-6)
- Produce content classified as a downstream-phase deliverable type (synthesis,
  draft, review, revision) even if you can see the end-goal
- Invoke or simulate any other agent persona's output (e.g., do not synthesize
  temporal patterns into prose claims — that is `synthesis_agent`'s Phase 3 work)
- "Helpfully" continue past your assigned deliverables

You MAY READ files in `phase1_*/` (Research Question Brief) and `phase2_*/` (own
phase, including annotated bibliography from `bibliography_agent` and verification
report from `source_verification_agent`) for legitimate context. Downstream phases
are not needed.

If downstream work is needed, return control to the caller with a recommendation.
Do not execute.

**Enforcement (v3.9.4):** prompt-level only. Advisory verifier
(`scripts/check_pipeline_integrity.py`) can detect violations post-hoc.
Deterministic PreToolUse hook deferred to v3.10 active conductor (#134).
```

**Output: `phase2_investigation/timeline.yaml`.** Three top-level keys:

```yaml
schema_version: "1.0"

sources:
  - citation_key: handbook-2024ed       # joins literature_corpus[].citation_key
    type: institutional-document        # informative; not gated
    published_date:
      value: "2024-09-15"
      precision: day                    # day | month | year | interval | unknown
      provenance:
        method: crossref_lookup          # crossref_lookup | pdftotext_cover |
                                         # user_override | adapter_metadata | unknown
                                         # (api_openalex reserved for v3.10)
        raw: "2024-09-15"
        source_locator: "doi:10.xxxx/handbook-2024"
        confidence: high                 # high | medium | low | unverified
    effective_date_range:
      start:
        value: "2024-10-01"
        precision: day
        provenance:
          method: pdftotext_cover
          raw: "Effective from October 1, 2024"
          source_locator: "file:///path/handbook-2024.pdf:p3"
          confidence: high
      end:
        open_ended: true                  # explicit field; distinct from precision:unknown
        value: null                       # null permitted only when open_ended=true
        precision: unknown                # unknown ONLY when open_ended=true; otherwise required precise
        provenance:
          method: user_override
          confidence: high                # confidence applies to the open-ended decision itself
    supersedes: handbook-2020ed          # citation_key of prior version OR null
    superseded_by: null
    version_family_id: handbook-family
    version_catalog_completeness: partial  # exhaustive | partial | unknown
                                            # (semantics not acted on in v3.9.4
                                            # — forward-compat only)

events:
  - event_id: programme-X-cycle-2022
    description: "Programme X review cycle 2022"
    date:
      value: "2022-04-01..2022-12-31"
      precision: interval
      provenance:
        method: user_override
        confidence: high
    governed_by: handbook-2020ed         # citation_key OR null
```

**Field discipline (load-bearing):**

- `precision` is REQUIRED on every date. `provenance.method` is REQUIRED on every date. `provenance.confidence: unverified` triggers `TEMPORAL-METADATA-MISSING` advisory at audit time when the date is used to verify a claim.

**Date shape table (closes F3 ambiguity — distinct representations, no null-overload).**

| Case | `precision` | `value` shape | `open_ended` | Verifier interval semantics |
|---|---|---|---|---|
| Day-precise | `day` | `"YYYY-MM-DD"` | `false` (or absent) | `[date, date]` point interval |
| Month-precise | `month` | `"YYYY-MM"` | `false` (or absent) | `[YYYY-MM-01, YYYY-MM-last]` |
| Year-precise | `year` | `"YYYY"` | `false` (or absent) | `[YYYY-01-01, YYYY-12-31]` |
| Interval-precise | `interval` | `"YYYY-MM-DD..YYYY-MM-DD"` (both endpoints inclusive) | `false` (or absent) | Parsed interval as written |
| Unknown date | `unknown` | `null` | `false` (or absent) | Triggers `TEMPORAL-METADATA-MISSING` when used in arithmetic |
| Open-ended range end | `unknown` | `null` | **`true`** | Verifier treats as `[start, +∞)` — still in force, NOT missing data |

The `open_ended: true` field appears ONLY on the `end` of `effective_date_range` (and v3.10 M5's planned `governed_by` range extension). For any other date, `open_ended` MUST be absent or `false`. Verifier disambiguates:

- `open_ended: true` → treat as still-in-force; do NOT emit `TEMPORAL-METADATA-MISSING`
- `open_ended: false` (or absent) AND `value: null` → treat as missing data; DO emit `TEMPORAL-METADATA-MISSING` when this date is used in arithmetic
- `open_ended: false` AND `value` is present → use the value
- `supersedes` / `superseded_by` join by `citation_key`. Cycles are forbidden (lint detects via `scripts/check_v3_9_4_timeline_consistency.py`).
- `version_catalog_completeness: exhaustive` is *recorded* in v3.9.4 but is *not acted on* by the verifier. Deferred to v3.10.

**How sources are populated** (all reads / writes done by `timeline_extraction_agent`; `bibliography_agent` is read-only context):

- `bibliography_agent`'s annotated bibliography already contains DOI per corpus entry; `timeline_extraction_agent` reads it as input and uses Crossref API `issued` field for `published_date` when DOI is present (see §3.4 M6 protocol — same Crossref call is the source for both `timeline.yaml.published_date` and `citation_provenance.yaml.crossref_issued`).
- For local PDFs, `pdftotext -f 1 -l 1 <pdf>` extracts cover page; first line matching `\b(20\d{2}|19\d{2})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b` is candidate `published_date` with `method: pdftotext_cover, confidence: medium`.
- User-provided overrides in `timeline.yaml` always win (`method: user_override`).
- Missing dates ARE permitted; downstream verifier emits `TEMPORAL-METADATA-MISSING` advisory rather than blocking.

### 3.2 M2 — Phase 4 → Phase 5 deterministic verifier

**Script.** `scripts/temporal_integrity_audit.py`. Runs after `report_compiler_agent` writes the draft, before Phase 5 review. Mirrors the v3.8 cite-time provenance finalizer slot.

**Inputs:**

- Finalized draft markdown with v3.7.3 `<!--ref:slug-->` markers
- `phase2_investigation/timeline.yaml`
- `phase2_investigation/citation_provenance.yaml` (from M6, §3.4)
- Material Passport `literature_corpus[]`

**Output: `phase4_composition/temporal_audit.md`** (human-readable) + **`phase4_composition/temporal_audit_results.yaml`** (machine-readable, schema below).

**Verification passes (run in order, all independent).** Each pass below has named-capture regexes and explicit predicate truth tables. Implementer writes the verifier directly from this section; no inference required.

#### P1 — Mode 1 future-as-past (`TEMPORAL-ARITHMETIC-IMPOSSIBLE`)

Sentence-level scan. For each sentence (split on `[.!?]` + line boundary), find all date mentions matching:

```
(?P<date>
  \b(?P<iso>\d{4}-\d{2}-\d{2})\b
  |
  \b(?P<month_year>(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4})\b
  |
  \b(?P<bare_year>(19|20)\d{2})\b
)
```

Each captured date is normalised to a `[start, end]` interval per §3.1 date shape table. A sentence with `< 2` distinct date captures is skipped (P1 emits no finding).

Sentences with `>= 2` dates are checked for one of two **violation patterns**. The patterns are written as Python `re` regex strings; `<D_anchor>` and `<D_event>` are placeholders that the implementer replaces with named captures referencing the `(?P<date>...)` group from the date-extraction pass above.

**Pattern A (retrospective claim about an already-completed event):**

```python
PATTERN_A = re.compile(
    r"(?:as of|on|in|reported in|stated in|noted in)\s+(?P<anchor>"
    r"\d{4}-\d{2}-\d{2}|(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}|(?:19|20)\d{2}"
    r").*?\b(?:had already|already|completed|finished|delivered)\b.*?(?P<event>"
    r"\d{4}-\d{2}-\d{2}|(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}|(?:19|20)\d{2}"
    r")",
    re.IGNORECASE | re.DOTALL,
)
```

Violation predicate: `event_interval.start > anchor_interval.end` (event start strictly after anchor end). Finding: `TEMPORAL-ARITHMETIC-IMPOSSIBLE`.

**Pattern B (prospective claim about a forthcoming event whose date has already passed):**

```python
PATTERN_B = re.compile(
    r"(?P<event>"
    r"\d{4}-\d{2}-\d{2}|(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}|(?:19|20)\d{2}"
    r")\s+(?:will be|to be|scheduled for|forthcoming|upcoming|planned).*?(?:as of|in|by)\s+(?P<anchor>"
    r"\d{4}-\d{2}-\d{2}|(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}|(?:19|20)\d{2}"
    r")",
    re.IGNORECASE | re.DOTALL,
)
```

Violation predicate: `event_interval.start <= anchor_interval.end` (forthcoming event already past at anchor time). Finding: `TEMPORAL-ARITHMETIC-IMPOSSIBLE`.

For sentences with `>= 2` dates that don't match either pattern: emit no finding. Sentences with `>= 3` dates: run both patterns; up to one finding per sentence; if multiple matches, keep the one with the largest `|event.start - anchor.end|` gap (most clearly impossible).

**Definite-pass semantics.** If the predicate evaluates false (event interval fits before/equal to anchor), no finding. If either date has `precision: unknown` and `open_ended: false` (missing data), emit `TEMPORAL-METADATA-MISSING` instead of `TEMPORAL-ARITHMETIC-IMPOSSIBLE`.

#### P2 — Mode 2 version-as-evidence-past (`TEMPORAL-ANACHRONISTIC-CITATION`)

For every `<!--ref:slug-->` marker in the draft:

1. Look up `slug` in `timeline.yaml` sources. If absent, emit `TEMPORAL-METADATA-MISSING` and continue (do not emit anachronism finding without a timeline entry).
2. Look up `effective_date_range.start` for the slug. Confidence is read from `start.provenance.confidence` (the canonical path per §3.1 date object model).
   - If `start` is absent OR `start.provenance.confidence == "unverified"` → emit `TEMPORAL-METADATA-MISSING` and continue (start is required for any anachronism direction).
   - If `start.provenance.confidence == "low"` → emit `TEMPORAL-METADATA-MISSING` and continue (low confidence dates are NOT used as ground truth for arithmetic; equivalent to unverified per §3.4 citation_provenance handling).
   - If `start.provenance.confidence ∈ {high, medium}` → proceed to step 3.
3. Look up `effective_date_range.end`. Three cases (per §3.1 date shape table):
   - `end.open_ended: true` → treat end as `+∞`; the "too old / superseded" check (step 5 second clause) is SKIPPED, but the "future version" check (step 5 first clause) still runs.
   - `end.value` is present AND `end.provenance.confidence ∈ {high, medium}` → use as upper bound for both checks.
   - `end` absent OR `end.provenance.confidence ∈ {low, unverified}` AND `open_ended: false` → "too old / superseded" check is SKIPPED (cannot tell); "future version" check still runs against `start`.
4. Extract the nearest cited event/case date in the same paragraph: scan ±200 characters around the ref marker for the same date regex as P1. If no event date found, emit no finding (cannot determine anachronism).
5. Predicates (both run; either trips a finding):
   - **Future version**: `effective_date_range.start > event_date.end` → emit `TEMPORAL-ANACHRONISTIC-CITATION` with rationale "cited version postdates event".
   - **Superseded version**: `effective_date_range.end < event_date.start` (only runs when `end.open_ended: false` AND `end.value` known) → emit `TEMPORAL-ANACHRONISTIC-CITATION` with rationale "cited version was superseded before event".

#### P3 — Mode 3 comparator hallucination partial (`TEMPORAL-COMPARATOR-UNMATERIALIZED`)

Pattern-detect comparator framing. Three named-capture regexes (run in order; first match wins for each sentence):

```
# Form A: "the prior edition" + year mentioned in surrounding ±60 chars
A: (?P<adj>prior|previous|earlier|older|preceding)\s+(?P<noun>edition|version|edition\s+\(\d{4}\)|version\s+\(\d{4}\))

# Form B: bare year + edition/version
B: \b(?P<year>(?:19|20)\d{2})\s+(?P<noun>edition|version|standard|handbook|guideline)\b

# Form C: "X edition of YYYY"
C: (?P<noun>edition|version|standard)\s+(?:of|from)\s+(?P<year>(?:19|20)\d{2})\b
```

For each match:

1. Identify the version_family_id. **Binding precedence:**
   - (i) If a `<!--ref:slug-->` marker appears in the same sentence, look up that slug's `version_family_id` in `timeline.yaml`.
   - (ii) Else if a `<!--ref:slug-->` appears in the same paragraph, use that slug's `version_family_id`.
   - (iii) Else: emit no finding (binding ambiguous — implementer cannot guess which family the comparator refers to).
2. Identify the comparator year:
   - For Form A: scan ±60 chars for a `\b(19|20)\d{2}\b` match; if absent, emit no finding.
   - For Form B/C: `<year>` capture is the comparator year.
3. Check `timeline.yaml` for a source entry with `version_family_id == <bound>` AND `published_date.value` parses to year `== <comparator_year>`.
4. If present → emit no finding (comparator exists in timeline).
5. If absent → emit `TEMPORAL-COMPARATOR-UNMATERIALIZED` with `matched_span` set to the regex match. (NOT phantom in v3.9.4 — v3.10 CC5 will distinguish.)

**Multiple comparator mentions per sentence:** each match independently runs the binding + check above; up to one finding per match.

#### P4 — Mode 4 causal inversion partial (`TEMPORAL-CAUSAL-INVERSION`)

Trigger-phrase scan with required-order map:

| Trigger phrase (regex) | Required ordering | Notes |
|---|---|---|
| `\benabled\b` | `left.date < right.date` | "A enabled B" → A before B |
| `\bcaused\b` | `left.date < right.date` | "A caused B" → A before B |
| `\bled\s+to\b` | `left.date < right.date` | "A led to B" → A before B |
| `\bin\s+response\s+to\b` | `left.date > right.date` | "A in response to B" → A after B |
| `\bsuperseded\b` | `left.date > right.date` | "A superseded B" → A after B |
| `\bpreceded\b` | `left.date < right.date` | "A preceded B" → A before B |
| `\bfollowed\s+by\b` | `left.date < right.date` | "A followed by B" → A before B (distinct from `followed`) |
| `\bfollowed\b(?!\s+by)` | `left.date > right.date` | "A followed B" → A after B (negative lookahead excludes Form above) |

For each match, identify left and right arguments:

- **Left**: the `<!--ref:slug-->` marker OR date capture closest preceding the trigger phrase, within the same sentence
- **Right**: the `<!--ref:slug-->` marker OR date capture closest following the trigger phrase, within the same sentence

If both bound to dates (via slug → timeline lookup or direct date capture): check predicate. Violation → emit `TEMPORAL-CAUSAL-INVERSION`.

If only one bound: emit no finding in v3.9.4 (implicit causality structurally out of scope; v3.10 M8 relation manifest will handle).

If neither bound (no slug, no date): emit no finding.

#### P5 — Mode 5 time-bomb (`TEMPORAL-DEICTIC`)

Deictic-phrase regex lint:

```
\b(currently|now|at present|most recent|the latest|new(?:est)?|recently|last\s+year|this\s+year|nowadays|presently|today|emerging|recent\s+cycle|latest\s+available)\b
```

Each match emits one `TEMPORAL-DEICTIC` finding with `matched_span` set to the regex match. Severity: `LOW`. User can choose to ack via `/ars-mark-read` or rewrite to anchored form.

**Output schema (sidecar aggregate, NOT a `defect_stage` of `claim_audit_result`).** Full per-finding shape with closed enums and per-kind required fields:

```yaml
schema_version: "1.0"
audit_run_id: "2026-05-18T12:34:56Z-a1b2"
report_reference_date: "2026-05-18"           # CC6 frozen — never wall-clock
findings:
  - finding_id: TF-001                        # pattern: ^TF-\d{3,}$
    finding_kind: TEMPORAL-ANACHRONISTIC-CITATION    # closed enum (6 values, below)
    severity: HIGH                            # closed enum: HIGH | MEDIUM | LOW
    mode: 2                                   # integer 1-5 (or null for METADATA-MISSING)
    block_eligible: true                      # forward-compat flag for v3.10 strict policy
                                              # (see eligibility table below; v3.9.4 always advisory regardless)
    draft_locator:
      file: "phase4_composition/draft.md"
      line: 142
      sentence: "The 2026 Handbook governed the 2022 review cycle."
    matched_span:                             # required for P3/P4/P5; null for P1/P2
      text: null
      char_start: null
      char_end: null
    bound_refs:                               # [] when none could bind
      - ref_slug: handbook-2026ed
        timeline_entry: handbook-2026ed
    bound_event:                              # null permitted; required for P2 only
      event_id: programme-X-cycle-2022
      date: "2022-04-01..2022-12-31"
    bound_dates: null                         # used by P1/P4; null for P2/P3/P5/MISSING
    rationale: "handbook-2026ed effective_date_range starts 2026-09-15, after event 2022-12-31."
    suggested_fix: "Cite handbook-2020ed (effective 2020-10-01..2024-09-30) for 2022 cycles."
```

**Finding-kind enum (v3.9.4 closed list of 6):**

- `TEMPORAL-ARITHMETIC-IMPOSSIBLE` (Mode 1)
- `TEMPORAL-ANACHRONISTIC-CITATION` (Mode 2)
- `TEMPORAL-COMPARATOR-UNMATERIALIZED` (Mode 3 partial)
- `TEMPORAL-CAUSAL-INVERSION` (Mode 4 partial)
- `TEMPORAL-DEICTIC` (Mode 5)
- `TEMPORAL-METADATA-MISSING` (verifier-deficiency advisory, not a violation; `mode: null` permitted)

**Per-kind required field map** (lint-enforced; full schema in `temporal_audit_results.schema.json` under `$defs.temporal_audit_finding`):

| Finding kind | `mode` | `bound_refs` | `bound_event` | `bound_dates` | `matched_span` | severity default |
|---|---|---|---|---|---|---|
| TEMPORAL-ARITHMETIC-IMPOSSIBLE | required (1) | `≥0` permitted | null permitted | required (anchor + event date pair from Pattern A/B match) | null | HIGH |
| TEMPORAL-ANACHRONISTIC-CITATION | required (2) | required (≥1) | required (non-null) | null permitted | null | HIGH |
| TEMPORAL-COMPARATOR-UNMATERIALIZED | required (3) | required (≥1, the bound version family ref) | null permitted | null permitted | required | MEDIUM |
| TEMPORAL-CAUSAL-INVERSION | required (4) | `≥0` permitted | null permitted | required (left/right dates from verb match; from refs OR direct date capture) | required | MEDIUM |
| TEMPORAL-DEICTIC | required (5) | [] | null | null permitted | required | LOW |
| TEMPORAL-METADATA-MISSING | null permitted | [] permitted | null permitted | null permitted | null permitted | LOW |

The new `bound_dates` field accommodates findings where the constraint involves a date pair not tied to a single corpus event:

```yaml
bound_dates:                              # null permitted (see map above)
  left:
    role: anchor                          # anchor | event | left_arg | right_arg
    value: "2025-03-01..2025-03-31"
    source: "draft_capture"               # draft_capture | timeline_ref | citation_provenance
    ref_slug: null                        # populated only when source == timeline_ref
  right:
    role: event
    value: "2025-06-01..2025-06-30"
    source: "draft_capture"
    ref_slug: null
```

P1 always populates `bound_dates` (anchor + event from Pattern A/B match — both come from `draft_capture` source). P4 always populates `bound_dates` (left + right argument dates — `source: "timeline_ref"` when bound via `<!--ref:slug-->` → timeline lookup, `source: "draft_capture"` when bound via direct date capture in the sentence). P4 `bound_refs` is populated only when slug-based binding is used; empty `[]` when both sides came from direct date captures. The aggregate schema's `oneOf` branching on `finding_kind` enforces the per-kind requirements in the table above.

**Block-eligibility forward-compat table (v3.9.4 records, v3.10 acts on):**

| Finding kind | `block_eligible` v3.9.4 value | v3.10 strict-mode behavior |
|---|---|---|
| TEMPORAL-ARITHMETIC-IMPOSSIBLE | `true` | Promoted to HIGH-BLOCK |
| TEMPORAL-ANACHRONISTIC-CITATION | `true` (only when both dates have `confidence: high`) | Promoted to HIGH-BLOCK |
| TEMPORAL-COMPARATOR-UNMATERIALIZED | `false` (until CC5 version_catalog_completeness ships) | Conditional |
| TEMPORAL-CAUSAL-INVERSION | `false` (LLM extraction in loop) | Stays advisory |
| TEMPORAL-DEICTIC | `false` (stylistic) | Stays advisory |
| TEMPORAL-METADATA-MISSING | `false` | Stays advisory |

**Advisory-only invariant (CC1):** No finding in v3.9.4 causes the formatter to refuse emission, regardless of `block_eligible: true`. The flag is recorded for v3.10 policy promotion only. The finalizer annotates `temporal_audit_findings_count: N` in the Phase 5 handoff but does not gate on it.

### 3.3 M3 — `report_compiler_agent` IRON RULE (prevention layer)

**Where:** New section in `deep-research/agents/report_compiler_agent.md` and `academic-paper/agents/draft_writer_agent.md`, named `## Temporal Integrity Iron Rule (v3.9.4)`.

**Content (verbatim block):**

```
## Temporal Integrity Iron Rule (v3.9.4)

Before writing any sentence that:

- Cites a document with a publication year via <!--ref:slug-->
- States that one event led to / was enabled by / superseded / followed another
- Uses present-tense or deictic framing ("currently", "now", "the most recent",
  "the latest", "new", "recently", "last year", "nowadays")
- Compares two versions of the same standard or document

You MUST:

1. Identify the date or date range of every entity in the claim (cited document,
   referenced event, comparator version) from `phase2_investigation/timeline.yaml`
   when available, or from corpus `year` field as a fallback (year-only interval).
2. Verify the cited document existed BEFORE the event it is being used to evidence
   (unless the research output is explicitly forward-looking about a forthcoming
   version, in which case explicitly note this).
3. For "A enabled B" / "A caused B" / "A led to B" framing, verify the date of A
   is before the date of B.
4. For "most recent" / "current" / "the latest" framing, anchor the claim to a
   specific date or version identifier ("as of YYYY-MM-DD, ..." or "the YYYY
   edition, ..."), not a deictic word.
5. If the dates required to verify the claim are absent from `timeline.yaml` and
   `literature_corpus[]`, either hedge ("appears to", "is reported as") or do
   NOT write the claim.

You may not rely on linguistic plausibility for temporal claims. Temporal claims
are arithmetic, not stylistic.
```

This mirrors the v3.7.3 R-L3-2 IRON RULE pattern (prompt-level prevention). It does not replace M2 (post-draft deterministic audit) — it reduces error rate so M2 has less to flag.

### 3.4 M6-minimal — Citation Provenance (owned by `timeline_extraction_agent`)

**Phase-boundary discipline (F2 closure).** Citation provenance verification is owned by `timeline_extraction_agent` (§3.1), NOT by `bibliography_agent`. Rationale: v3.9.2 phase-boundary spec locks `bibliography_agent`'s sole deliverable to "Annotated Bibliography + Search Strategy report"; adding `citation_provenance.yaml` as a new deliverable would violate that lock. `timeline_extraction_agent` is the new Phase 2 sibling and its v3.9.4 deliverables explicitly include both `timeline.yaml` and `citation_provenance.yaml`. `bibliography_agent` is NOT modified in v3.9.4.

**Where:** New section in `deep-research/agents/timeline_extraction_agent.md` named `## Citation Provenance Protocol (v3.9.4)`.

**Output: separate sidecar `phase2_investigation/citation_provenance.yaml`** — NOT added to `literature_corpus_entry.schema.json` (which is `additionalProperties: false` adapter-owned closed contract).

**Sidecar schema:**

```yaml
schema_version: "1.0"
audit_run_id: "2026-05-18T12:34:56Z-a1b2"
entries:
  - citation_key: handbook-2024ed             # joins literature_corpus[]
    crossref_issued:                          # null if no DOI in corpus entry
      value: "2024-09-15"
      precision: day                          # day | month | year (from Crossref granularity)
      verified_at: "2026-05-18T12:34:56Z"
      api_endpoint: "https://api.crossref.org/works/10.xxxx/handbook-2024"
    pdftotext_cover_first_line:               # null if no local PDF in source_pointer
      line: "Quality Assurance Handbook, 2024 Edition"
      published_date_candidate:
        value: "2024"
        precision: year                       # candidate extracted from first-line scan
      verified_at: "2026-05-18T12:34:56Z"
      pdf_path: "/path/handbook-2024.pdf"
    verification_method: crossref_and_pdftotext  # closed enum (see below)
    confidence: high                          # closed enum (see agreement table below)
    notes: null
```

**Closed enums:**

- `verification_method` ∈ `crossref_and_pdftotext` | `crossref_only` | `pdftotext_only` | `none`
- `confidence` ∈ `high` | `medium` | `low` | `conflict`

**Process (`timeline_extraction_agent` runs at ingest time):**

1. For every corpus entry with `doi`: call `https://api.crossref.org/works/<DOI>` and record `message.issued.date-parts[0]` as `crossref_issued.value`. Precision = `day` if all 3 date parts present; `month` if 2; `year` if 1.
2. For every corpus entry with local PDF in `source_pointer` (file://): run `pdftotext -f 1 -l 1 <pdf>` and record the first non-empty line. Extract a `published_date_candidate` if a 4-digit year matching `\b(19\d{2}|20\d{2})\b` appears in the line; precision is `year` unless an ISO-8601 date is found verbatim.
3. Compute `confidence` per the agreement table below.
4. Output written to `phase2_investigation/citation_provenance.yaml`.

**Confidence agreement table (F9 closure).**

Inputs to the table are three source states: corpus `year` (always present per `literature_corpus_entry.schema.json` requirement), Crossref `issued.year` (present if DOI lookup succeeded), and pdftotext first-line `year` (present if PDF scan succeeded and a year was extracted).

| Corpus | Crossref | pdftotext | All present and agree? | `confidence` | `verification_method` | `notes` |
|---|---|---|---|---|---|---|
| Y | Y | Y | All 3 years equal | `high` | `crossref_and_pdftotext` | null |
| Y | Y | Y | Any disagree | `conflict` | `crossref_and_pdftotext` | null |
| Y | Y | absent (no PDF in source_pointer) | Corpus year == Crossref year | `medium` | `crossref_only` | null |
| Y | Y | absent | Disagree | `conflict` | `crossref_only` | null |
| Y | absent (no DOI in corpus) | Y | Corpus year == pdftotext year | `medium` | `pdftotext_only` | null |
| Y | absent | Y | Disagree | `conflict` | `pdftotext_only` | null |
| Y | absent | absent | — (only corpus year, nothing to verify against) | `low` | `none` | null |
| Y | API outage | absent | Cannot verify; no PDF | `low` | `none` | `"crossref_outage"` |
| Y | API outage | Y | Corpus year == pdftotext year | `medium` | `pdftotext_only` | `"crossref_outage"` |
| Y | API outage | Y | Corpus year != pdftotext year | `conflict` | `pdftotext_only` | `"crossref_outage"` |

"Crossref outage" is detected by HTTP 5xx, connection error, or wrapper-level timeout (configurable, default 10s). Crossref returning HTTP 404 for a valid-looking DOI is NOT outage — it counts as Crossref "absent" (the DOI may be malformed or unminted). DOI present but Crossref 404 → row 5 / 6 / 7 of the table, NOT row 8 / 9 / 10.

"Year equal" means the 4-digit year integers match. v3.9.4 does NOT compare month or day for agreement — `crossref_issued: 2024-09-15` and `pdftotext: 2024` both count as "2024" and agree. v3.10 may tighten this when full PDF cover audit lands.

**What M2 (§3.2) does with this:**

- For each `<!--ref:slug-->` in the draft, M2 looks up the slug's `confidence` in `citation_provenance.yaml`.
- `low` or `conflict` → emit `TEMPORAL-METADATA-MISSING` advisory at audit time for any temporal claim against this slug.
- `high` or `medium` → use the dates as ground truth for arithmetic.

This closes the "verifier becomes deterministic arithmetic over LLM-confabulated dates" gap: dates that have not been first-party-verified are flagged before being used as ground truth.

**Out of v3.9.4 scope:** full PDF cover audit (title/authors/venue cross-check); OpenAlex `publication_date` lookup. v3.9.4 ships **Crossref issued-date + pdftotext first-line scan only**. v3.10 extends to OpenAlex and full bibliographic cross-check.

### 3.5 M7-minimal — Temporal fact provenance + comparator materialization

This is split into two pieces, both already specified above and named here for naming-stability:

**M7a — Date provenance** (already in §3.1): every date in `timeline.yaml` carries `provenance.method` + `provenance.confidence`. Verifier in §3.2 treats `confidence: unverified` dates as not-ground-truth.

**M7b — Comparator materialization** (already in §3.2 P3): when prose names a comparator version ("the 2022 edition"), the verifier requires a matching timeline entry. Absence emits `TEMPORAL-COMPARATOR-UNMATERIALIZED` regardless of `version_catalog_completeness`. This is *claim-support* advisory ("you've referenced something we have no record of"), not *phantom-existence* judgment ("this version doesn't exist") — the latter requires `version_catalog_completeness: exhaustive` and is deferred to v3.10 CC5.

### 3.6 M5-stub — version-family awareness (owned by `timeline_extraction_agent`)

**Phase-boundary discipline (F2 closure).** Version-family awareness is owned by `timeline_extraction_agent`, NOT `bibliography_agent`. The stub records `version_family_id` directly in `timeline.yaml` entries; `bibliography_agent`'s annotated bibliography is unmodified.

**v3.9.4 ships a stub:** `timeline_extraction_agent` records `version_family_id` in `timeline.yaml` when it observes a known supersession relationship in the corpus. The stub does *not* enforce that all versions in a family are catalogued — that is v3.10 M5 full.

Concretely:

- When `timeline_extraction_agent` processes "Handbook 2024 Edition" (corpus entry `handbook-2024ed`), it sets `version_family_id: handbook-family` in the timeline source entry.
- If the corpus also contains "Handbook 2020 Edition" (corpus entry `handbook-2020ed`), `version_family_id: handbook-family` matches, and `supersedes: handbook-2020ed` is recorded.
- If only the 2024 edition is in the corpus and prose mentions the 2020 edition, M2 P3 fires `TEMPORAL-COMPARATOR-UNMATERIALIZED` (via §3.5 M7b).

**Version-family identification** in v3.9.4 is **user-declared, not inferred**. `timeline_extraction_agent` records `version_family_id` only when the user has explicitly tagged corpus entries with a shared family hint (e.g., via a `tags: ["version-family:handbook"]` corpus field, which `literature_corpus_entry.schema.json` already permits as free-form). v3.10 M5-full will add proactive title-similarity-based version-family discovery; v3.9.4 does NOT attempt this (avoids false joins between "Handbook of Economics" and "Handbook of Education").

---

## 4. Schema additions

**Zero modification to existing schemas.** All v3.9.4 additions are new sidecar artifacts with their own schema files. Each schema is **aggregate-level** (validates the whole file), with per-entry shapes declared in `$defs`:

| New file | Validates | `$defs` |
|---|---|---|
| `shared/contracts/passport/timeline.schema.json` | The entire `phase2_investigation/timeline.yaml` aggregate (top-level `schema_version` + `sources[]` + `events[]`) | `timeline_source_entry`, `timeline_event_entry`, `date_object`, `provenance_object` |
| `shared/contracts/passport/citation_provenance.schema.json` | The entire `phase2_investigation/citation_provenance.yaml` aggregate (top-level `schema_version` + `audit_run_id` + `entries[]`) | `citation_provenance_entry` |
| `shared/contracts/passport/temporal_audit_results.schema.json` | The entire `phase4_composition/temporal_audit_results.yaml` aggregate (top-level `schema_version` + `audit_run_id` + `report_reference_date` + `findings[]`) | `temporal_audit_finding`, with `oneOf` branching per `finding_kind` to enforce the per-kind required-field map in §3.2 |

All three schemas use `"$schema": "https://json-schema.org/draft/2020-12/schema"` and set `additionalProperties: false` on every object level (closed contracts, mirroring existing passport schemas).

**Join keys.** `citation_key` is the **sole** join key across corpus ↔ timeline ↔ citation_provenance for source-bound facts. Timeline events (entries in `events[]` rather than `sources[]`) carry an `event_id` field (pattern `^[A-Za-z][A-Za-z0-9_:-]*$`, mirroring `literature_corpus_entry.citation_key`'s pattern for shape uniformity — examples: `programme-X-cycle-2022`, `decree-A-revocation`, `accreditation-round-2023`); event entries do NOT carry `citation_key` (events are not bound to a specific bibliographic citation; they reference sources via the `governed_by` field). `audit_run_id` joins per-run artifacts (citation_provenance ↔ temporal_audit_results).

**Existing schemas explicitly unchanged:**

- `literature_corpus_entry.schema.json` (`additionalProperties: false` adapter-owned closed contract) — **unchanged**
- `claim_audit_result.schema.json` `defect_stage` enum (8 values) — **unchanged** (temporal findings ride in the v3.9.4 sidecar aggregate, not as a `defect_stage`)
- `claim_intent_manifest.schema.json` `intended_evidence_kind` enum (4 values) — **unchanged**

This is the critical invariant: temporal verification is an independent sidecar layer, not a deformation of v3.7.3 / v3.8 / v3.9.0 contracts.

---

## 5. Coverage estimate (honest)

This section is the calibrated coverage estimate after dual-track design challenge. Earlier drafts of this design (pre-challenge) carried an optimistic 75-85% estimate. Codex R2 push-back lowered this to the following:

| Failure mode | v3.9.4 net coverage | What escapes |
|---|---|---|
| Mode 1 future-as-past | **75-85%** | Year-only dates / "following summer" / academic-fiscal years / planned-vs-completed ambiguity / event dates in source text but absent from timeline. Verifier needs two explicit dates with precision ≥ year. |
| Mode 2 version-as-evidence-past | **45-60%** | `effective_date_range` is often missing or only inferable from publication date. Publication date ≠ effective date. When effective range is missing, verifier emits `TEMPORAL-METADATA-MISSING` not violation — the prose can still be wrong, just unflagged. |
| Mode 3 comparator hallucination | **20-35%** without M7b / **45-60%** with M7b | M6 first-party only verifies entries in the corpus; phantom comparators are by definition not in the corpus. M7b (comparator materialization) catches "you cited X but no timeline entry exists" — this is claim-support advisory, not phantom-existence proof. v3.10 CC5 + `version_catalog_completeness` semantics will distinguish phantom from uncatalogued. |
| Mode 4 causal inversion | **25-40%** | Explicit causal verbs (enabled/caused/led to) catchable. Implicit framing ("Subsequently, X.") with no trigger phrase is structurally not catchable in v3.9.4. v3.10 M8 relation manifest may push this to 35-50%. |
| Mode 5 time-bomb | **70-85%** | "newer" / "emerging" / "recent cycle" / "latest available" / "the present framework" / "nowadays" — variants beyond the regex list always escape. Lint can be extended round-by-round. |
| **Net v3.9.4** | **55-70%** | LLM extractor blindness + missing timeline metadata + phantom-comparator structural limit |
| Net v3.10 projection | **65-80%** | M4 reviewer + M5 full + M6 full PDF + M8 relation manifest + CC5 catalog semantics |

**Structural limits (cannot be closed by any v3.x patch):**

- LLM next-token objective is structurally biased toward fluent prose; any verifier with an LLM-tuple-extraction step inherits this bias on the extraction layer.
- Author chain and reviewer chain are both LLMs and share blind spots — adding more reviewer rounds does not converge on temporal correctness.
- Hard arithmetic verification requires structured ground truth (timeline.yaml). When ground truth is missing, the only safe move is `TEMPORAL-METADATA-MISSING` advisory, which surfaces uncertainty but cannot create truth.

The advisory architecture is the honest acknowledgement of this: ARS exposes structured uncertainty rather than fabricating false confidence.

---

## 6. Cross-cutting decisions

| ID | Decision | Status |
|---|---|---|
| CC1 | All v3.9.4 findings are advisory. No `HIGH-BLOCK` tier. Formatter refusal list unchanged from v3.9.0. | Locked |
| CC2 | `temporal_audit_results[]` is a sibling sidecar aggregate. NOT a `defect_stage` of `claim_audit_result.schema.json`. | Locked |
| CC3 | `TEMPORAL-*` finding kind family is independent from v3.7.3 / v3.9.0 `CONTAMINATED-*` family. No overlap. | Locked |
| CC4 | Every date in `timeline.yaml` REQUIRES `precision` ∈ {day, month, year, interval, unknown}. When `precision: unknown` AND `open_ended: false` (or absent), the date is treated as missing data — verifier emits `TEMPORAL-METADATA-MISSING` when this date is used in arithmetic. When `precision: unknown` AND `open_ended: true`, the date is treated as `+∞` (still in force; only valid for `effective_date_range.end`). No other date may carry `open_ended: true`. Per §3.1 date shape table. | Locked |
| CC5 | `version_catalog_completeness: exhaustive` semantics (phantom-vs-uncataloged disambiguation). | **Deferred to v3.10** |
| CC6 | Tests freeze `report_reference_date` — never use wall-clock today. Audit output records the frozen date. | Locked |
| CC7 | Legacy `literature_corpus[]` entries stay legacy. Year-only fallback interval (`year: 2024` → `2024-01-01..2024-12-31`). Opt-in bootstrap script for `timeline.yaml`. Corpus never mutated. | Locked |

---

## 7. Lint & CI

**New lint:** `scripts/check_v3_9_4_temporal_verification.py`. Validates:

1. `timeline.yaml` schema conformance against `timeline.schema.json` (aggregate, with per-entry shapes in `$defs`).
2. Supersession chain has no cycles (recursive `supersedes`/`superseded_by`).
3. Every date carries `precision` and `provenance.method`.
4. `citation_provenance.yaml` schema conformance against `citation_provenance.schema.json`.
5. `temporal_audit_results.yaml` schema conformance against `temporal_audit_results.schema.json` + `finding_kind` ∈ closed list of 6 + per-kind required-field map per §3.2 enforced via the schema's `oneOf` branching on `finding_kind`.
6. M3 IRON RULE block present in `deep-research/agents/report_compiler_agent.md` and `academic-paper/agents/draft_writer_agent.md` (verbatim string match against the §3.3 block content).
7. M6 Citation Provenance Protocol section present in `deep-research/agents/timeline_extraction_agent.md` (NOT bibliography_agent — F2 boundary-discipline closure).
8. `bibliography_agent.md` is verified UNMODIFIED relative to its v3.9.2 baseline (git-diff check or content hash) — closes the F2 invariant that v3.9.4 does not violate `bibliography_agent`'s sole-deliverable boundary.

**Existing lint not modified:**

- `scripts/check_v3_7_3_three_layer_citation.py` (anchor channel) — unchanged.
- `scripts/check_v3_9_0_triangulation.py` (contamination triangulation) — unchanged.
- `scripts/check_claim_audit_consistency.py` (v3.8 claim audit) — unchanged. The temporal sidecar does not interact with claim_audit invariants.

**Existing lint modified — `scripts/check_v3_9_2_phase_boundary.py`** (4-touchpoint update; not a single-line addition):

1. **Add `"deep-research/agents/timeline_extraction_agent.md"` to the hardcoded `BUCKET_A_AGENTS` list.** Bucket A count rises from 22 to 23.
2. **Update the Bucket A sanity-check assertion** (`assert len(BUCKET_A_AGENTS) == 22`) to `== 23`. The lint hard-codes the count to detect accidental list edits; v3.9.4 explicitly bumps it.
3. **Extend the block-marker keyword scan** to accept either `Phase Boundary (v3.9.2)` or `Phase Boundary (v3.9.4)` as the H2 marker. The four load-bearing keywords (`Phase Boundary (vX.X.X)`, `MUST NOT`, `MAY READ`, `Enforcement (vX.X.X)`) remain required; only the version suffix is widened.
4. **Update `docs/design/2026-05-18-ars-v3.9.2-agent-phase-classification.md`** to list `timeline_extraction_agent` under Bucket A (Phase 2). The classification doc is the source-of-truth `BUCKET_A_AGENTS` mirrors.

Test fixture update: `scripts/test_check_v3_9_2_phase_boundary.py` (if it asserts the 22-count or enumerates Bucket A agents) gets the same count bump. A new positive-test fixture for `timeline_extraction_agent` exercising the v3.9.4 boundary block is added.

**CI workflow:** `.github/workflows/spec-consistency.yml` gains one job invoking the new lint over `tests/fixtures/v3.9.4-temporal/` fixture directory.

---

## 8. Test fixtures

All fixtures live under `tests/fixtures/v3.9.4-temporal/`. Each fixture is a directory containing a 4-tuple of files (the input triplet + expected output):

```
tests/fixtures/v3.9.4-temporal/<fixture_name>/
├── draft.md                                       # Phase 4 draft input
├── timeline.yaml                                  # phase2_investigation/timeline.yaml input
├── citation_provenance.yaml                       # phase2_investigation/citation_provenance.yaml input
└── expected_temporal_audit_results.yaml           # golden output for diff comparison
```

Five **positive fixtures** (each must trigger exactly one finding of the named kind):

| Fixture name | draft.md content (truncated) | timeline.yaml content (truncated) | Expected finding kind | Expected `bound_refs` / `bound_event` |
|---|---|---|---|---|
| `mode_1_future_as_past/` | `"As of March 2025, the report noted that the system had already completed June 2025 deliverables."` | (no handbook needed; sentence is self-contained arithmetic) | `TEMPORAL-ARITHMETIC-IMPOSSIBLE` | `bound_refs: []`; `bound_event: null`; `bound_dates: {left: {role: "anchor", value: "2025-03-01..2025-03-31", source: "draft_capture"}, right: {role: "event", value: "2025-06-01..2025-06-30", source: "draft_capture"}}` — matches Pattern A: anchor="As of March 2025", verb="had already completed", event="June 2025" |
| `mode_2_version_as_evidence_past/` | `"The 2026 Handbook governed the 2022 review cycle.<!--ref:handbook-2026ed-->"` | handbook-2026ed source with `effective_date_range.start: 2026-09-15`; event programme-X-cycle-2022 with `date: "2022-04-01..2022-12-31"` | `TEMPORAL-ANACHRONISTIC-CITATION` | `bound_refs: [handbook-2026ed]`; `bound_event: programme-X-cycle-2022` |
| `mode_3_comparator_unmaterialized/` | `"This differs from the 1998 edition of the standard.<!--ref:standard-2020ed-->"` | standard-2020ed entry with `version_family_id: standard-family`; no entry for 1998 | `TEMPORAL-COMPARATOR-UNMATERIALIZED` | `bound_refs: [standard-2020ed]`; `matched_span` contains "1998 edition" |
| `mode_4_causal_inversion/` | `"Policy A<!--ref:policy-a--> enabled Policy B<!--ref:policy-b-->."` | policy-a `published_date: 2026-03-01`; policy-b `published_date: 2020-05-15` | `TEMPORAL-CAUSAL-INVERSION` | `bound_refs: [policy-a, policy-b]`; `bound_event: null`; `bound_dates: {left: {role: "left_arg", value: "2026-03-01", source: "timeline_ref", ref_slug: "policy-a"}, right: {role: "right_arg", value: "2020-05-15", source: "timeline_ref", ref_slug: "policy-b"}}`; rationale notes left.date > right.date violates `enabled` predicate |
| `mode_5_time_bomb/` | `"Currently, the most recent edition prescribes annual review."` | (irrelevant for P5 — pure regex lint) | `TEMPORAL-DEICTIC` | `bound_refs: []`; `matched_span` contains "Currently, the most recent" |

Five **false-positive fixtures** (legitimate prose that must produce zero findings):

| Fixture name | draft.md content (truncated) | timeline.yaml | Why it must NOT trigger |
|---|---|---|---|
| `mode_1_legitimate/` | `"Smith (2026) reports that programme X completed its 2025 cycle in December 2025."` | smith-2026 published 2026 | Sentence has 2 dates but no pattern A/B match (no "as of", no "had already"); P1 emits no finding |
| `mode_2_legitimate/` | `"The 2020 Handbook governed the 2022 review cycle.<!--ref:handbook-2020ed-->"` | handbook-2020ed `effective_date_range: 2020-10-01..2024-09-30` | effective range covers event start; no anachronism |
| `mode_3_legitimate/` | `"This differs from the 2018 edition.<!--ref:standard-2020ed-->"` | both standard-2020ed AND standard-2018ed in timeline with shared `version_family_id` | comparator materialized; P3 emits no finding |
| `mode_4_legitimate/` | `"Policy A<!--ref:policy-a--> enabled Policy B<!--ref:policy-b-->."` | policy-a 2020-05-15; policy-b 2026-03-01 | A before B as required; predicate satisfied |
| `mode_5_legitimate/` | `"As of 2026-05-18, the 2024 edition prescribes annual review."` | (irrelevant) | "As of 2026-05-18" and "2024 edition" are anchored; deictic regex does NOT match (no "currently", "most recent", etc.) |

**`TEMPORAL-METADATA-MISSING` fixture** (`metadata_missing_p2/`): draft cites `<!--ref:handbook-2024ed-->` against 2022 event, but `handbook-2024ed` has `effective_date_range: null`. Expected finding kind: `TEMPORAL-METADATA-MISSING`, `bound_refs: [handbook-2024ed]`.

**Regression fixture `report_reference_date_freeze/`** (CC6): uses `report_reference_date: 2026-05-18` in expected output. Test asserts that running the audit on different wall-clock dates produces byte-identical `expected_temporal_audit_results.yaml`.

**Golden test execution.** For each fixture:

```python
def test_<fixture_name>():
    result = run_audit(
        draft="tests/fixtures/v3.9.4-temporal/<fixture_name>/draft.md",
        timeline="tests/fixtures/v3.9.4-temporal/<fixture_name>/timeline.yaml",
        provenance="tests/fixtures/v3.9.4-temporal/<fixture_name>/citation_provenance.yaml",
        report_reference_date="2026-05-18",
    )
    expected = yaml.safe_load(open("tests/fixtures/v3.9.4-temporal/<fixture_name>/expected_temporal_audit_results.yaml"))
    assert result == expected  # byte-exact diff
```

Total fixture count: 5 positive + 5 false-positive + 1 metadata-missing + 1 freeze regression = **12 fixtures**.

---

## 9. Migration

**No automatic migration.** Legacy `literature_corpus[]` entries are not mutated and continue to work via year-only fallback interval semantics.

**Opt-in bootstrap script:** `scripts/bootstrap_timeline_yaml.py` walks `literature_corpus[]` and:

1. For each entry with a `doi`, calls Crossref API to populate `published_date` in `timeline.yaml`.
2. For each entry with a local PDF in `source_pointer`, runs `pdftotext` for first-line scan.
3. Emits a `timeline.yaml` with `effective_date_range`, `supersedes`, `superseded_by`, `version_family_id`, `version_catalog_completeness` all left as `null` / `unknown` — user must manually populate these via a separate editing pass.
4. Idempotent. Re-running over an existing `timeline.yaml` only adds entries; never overwrites user-set fields.

**The boundary:** Crossref can populate `published_date` (precision: day or month). Crossref CANNOT populate `effective_date_range` (the period a handbook governs cases). The bootstrap script makes this asymmetry explicit and does not invent effective ranges. (v3.10 will add OpenAlex `publication_date` as a second source for `published_date`; the asymmetry against `effective_date_range` still applies.)

---

## 10. Implementation plan

This spec is shipping under writing-plans skill discipline. Estimated effort: 4-6 working days for impl phase after spec ships:

- Day 1-2: aggregate schemas (`timeline.schema.json`, `citation_provenance.schema.json`, `temporal_audit_results.schema.json`) + `scripts/check_v3_9_4_temporal_verification.py` lint + 12 fixture directories per §8
- Day 2-3: `scripts/temporal_integrity_audit.py` (the §3.2 5-pass verifier) + golden tests
- Day 3-4: `deep-research/agents/timeline_extraction_agent.md` (new agent — includes Phase 2 boundary block, Citation Provenance Protocol per §3.4, and Version-Family stub per §3.6) + M3 IRON RULE block additions to `deep-research/agents/report_compiler_agent.md` and `academic-paper/agents/draft_writer_agent.md`. **`bibliography_agent.md` is NOT modified** (F2 invariant).
- Day 4-5: `scripts/bootstrap_timeline_yaml.py` (Crossref + pdftotext)
- Day 5-6: CI workflow integration + dual-track codex+gemini PR review to 0 P1/P2 ship gate

The dual-track design-phase review for this spec runs after spec ships; pre-impl gate is the spec-review track. Impl PR runs its own dual-track gate.

---

## 11. Risks & open questions

**R1.** `pdftotext` quality is inconsistent across PDF generators (Adobe InDesign / Word export / scanned + OCR). First-line scan may return garbage. Mitigation: M6 emits `confidence: low` rather than wrong dates; M2 treats `confidence: low` as `TEMPORAL-METADATA-MISSING`.

**R2.** Crossref API rate limits and outages. Mitigation: M6 is idempotent + cached in `citation_provenance.yaml`; outage simply leaves entries at `confidence: low` until re-run.

**R3.** Sentence-level date extraction in M2 P1 (Mode 1 verifier) uses regex on prose. Regex misses paraphrased dates ("in early 2025", "last quarter of 2024"). Mitigation: M2 emits findings only when the arithmetic is unambiguous; ambiguous prose generates no finding (false negatives, not false positives).

**R4.** Implicit causality (Mode 4 without trigger phrase) is structurally not catchable in v3.9.4. Acknowledged in §5. Defer to v3.10 M8 relation manifest.

**R5.** The `version_family_id` join in v3.9.4 is **user-declared only** (`timeline_extraction_agent` reads `tags: ["version-family:<id>"]` hints from corpus entries; no title-similarity inference). Risk: user forgets to tag related editions → M7b emits `TEMPORAL-COMPARATOR-UNMATERIALIZED` against a comparator that actually exists in the corpus under a different `citation_key`. Mitigation: M7b is advisory; user can fix by adding the tag or by adding the `supersedes`/`superseded_by` link directly in `timeline.yaml`. v3.10 M5-full will add proactive title-similarity-based discovery.

**OQ1.** Should M3 IRON RULE live only in `report_compiler_agent.md` and `draft_writer_agent.md`, or also in `synthesis_agent.md`? Synthesis produces prose with dates. Current spec scopes to compiler+writer; synthesis_agent producing dated claims is followed by compiler/writer downstream, so IRON RULE may be redundant. To be settled in dual-track spec review.

---

## 12. Versioning & rollout

- Bumps `MODE_REGISTRY.md` ARS version from `3.9.3` to `3.9.4`.
- Bumps `README.md` / `README.zh-TW.md` / `academic-pipeline/SKILL.md` / `.claude-plugin/plugin.json` / `scripts/check_spec_consistency.py` expected-text strings per `feedback_version_bump_sweep_checklist`.
- Tag `v3.9.4` after PR merges and 0-finding codex+gemini dual-track PR review.
- No flag-gating. v3.9.4 ships ON by default. The advisory nature (CC1) means defaulting to ON does not change emission behavior; it only adds artifacts that users can choose to consume.
- ars-codex sibling repo sync deferred to a separate vendor PR after main lands.

---

## 13. References to companion specs and memory

- v3.7.3 spec — `docs/design/2026-05-12-ars-v3.7.3-claim-faithfulness-and-contaminated-source-spec.md` (advisory measurement precedent; contamination_signals + finalizer matrix)
- v3.8 audit-spec — `docs/design/2026-05-15-issue-103-claim-alignment-audit-spec.md` (cite-time finalizer Phase 4 → 5 slot + sibling-aggregate split discipline)
- v3.9.0 spec — `docs/design/2026-05-17-ars-v3.9.0-cross-index-triangulation-measurement-spec.md` (additive extension template; R-L3-2-A firm-rule preservation pattern)
- v3.9.2 spec — `docs/design/2026-05-18-ars-v3.9.2-phase-boundary-spec.md` (Phase 2 single-phase agent discipline)
- #134 active conductor — GitHub issue (will subsume hard-block policy under structured task envelopes in v3.10)
- #135 originating issue — GitHub issue (full 5-mode failure analysis)
