← Về thư mục
📄 / / academic-research-skills / docs / design / 2026-05-18-ars-v3.9.4-temporal-verification-spec.md

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:

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:

2.3 v3.10 forward scope (acceptance criteria)

Superseded (framing only) for the shared-field rows by docs/design/2026-05-31-ars-v3.10-policy-layer-rescope-spec.md. The "Hard-block policy" and "R-L3-2-A reword" rows below assume a single shared triangulation_policy field gaining a strict_temporal value. A first-party audit (2026-05-31) found this collides with the v3.9.0 spec's same-named triangulation_policy field, and that this spec's assertion that "#134 active conductor will subsume hard-block policy in v3.10" is a dangling claim (#134's issue body never staked that scope). The v3.10 rescope instead introduces a namespaced terminal_policies object whose temporal_integrity key is forward-reserved with only advisory accepted in v3.10 — temporal strict wiring (Mode 1 / Mode 2 → HIGH-BLOCK) is explicitly OUT of v3.10 scope and remains a v3.9.4 follow-up. The broad R-L3-2-A reword (contamination AND temporal) IS adopted in v3.10, but the wording explicitly states no temporal strict path exists yet (no over-promise). See the rescope spec §1.1 + §2 (D1) + §6.

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:

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):

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:

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

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:

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):

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):

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).
  3. If start is absent OR start.provenance.confidence == "unverified" → emit TEMPORAL-METADATA-MISSING and continue (start is required for any anachronism direction).
  4. 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).
  5. If start.provenance.confidence ∈ {high, medium} → proceed to step 3.
  6. Look up effective_date_range.end. Three cases (per §3.1 date shape table):
  7. 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.
  8. end.value is present AND end.provenance.confidence ∈ {high, medium} → use as upper bound for both checks.
  9. 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.
  10. 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).
  11. Predicates (both run; either trips a finding):
  12. Future version: effective_date_range.start > event_date.end → emit TEMPORAL-ANACHRONISTIC-CITATION with rationale "cited version postdates event".
  13. 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:
  2. (i) If a <!--ref:slug--> marker appears in the same sentence, look up that slug's version_family_id in timeline.yaml.
  3. (ii) Else if a <!--ref:slug--> appears in the same paragraph, use that slug's version_family_id.
  4. (iii) Else: emit no finding (binding ambiguous — implementer cannot guess which family the comparator refers to).
  5. Identify the comparator year:
  6. For Form A: scan ±60 chars for a \b(19|20)\d{2}\b match; if absent, emit no finding.
  7. For Form B/C: <year> capture is the comparator year.
  8. Check timeline.yaml for a source entry with version_family_id == <bound> AND published_date.value parses to year == <comparator_year>.
  9. If present → emit no finding (comparator exists in timeline).
  10. 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:

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:

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):

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:

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:

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:

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:

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:

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:

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):

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:

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:

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:

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


13. References to companion specs and memory