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.
| 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.
Synthesised from observed cases:
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.
Add a deterministic temporal-integrity verification layer that:
phase2_investigation/timeline.yaml — never mutates literature_corpus_entry.schema.json (which is additionalProperties: false adapter-owned).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).phase2_investigation/citation_provenance.yaml — so M2's deterministic verifier does not become "precise arithmetic over LLM-confabulated dates."report_compiler_agent (prevention, not verification — prompt-level reduction of error rate).precision everywhere a date appears in the timeline (day / month / year / interval / unknown) to prevent fake precision.The following are explicitly out of v3.9.4 scope:
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.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.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.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.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.)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.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 sharedtriangulation_policyfield gaining astrict_temporalvalue. A first-party audit (2026-05-31) found this collides with the v3.9.0 spec's same-namedtriangulation_policyfield, 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 namespacedterminal_policiesobject whosetemporal_integritykey is forward-reserved with onlyadvisoryaccepted in v3.10 — temporalstrictwiring (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." |
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.
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):
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-MISSINGopen_ended: false (or absent) AND value: null → treat as missing data; DO emit TEMPORAL-METADATA-MISSING when this date is used in arithmeticopen_ended: false AND value is present → use the valuesupersedes / 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).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.timeline.yaml always win (method: user_override).TEMPORAL-METADATA-MISSING advisory rather than blocking.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:
<!--ref:slug--> markersphase2_investigation/timeline.yamlphase2_investigation/citation_provenance.yaml (from M6, §3.4)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.
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.
TEMPORAL-ANACHRONISTIC-CITATION)For every <!--ref:slug--> marker in the draft:
slug in timeline.yaml sources. If absent, emit TEMPORAL-METADATA-MISSING and continue (do not emit anachronism finding without a timeline entry).effective_date_range.start for the slug. Confidence is read from start.provenance.confidence (the canonical path per §3.1 date object model).start is absent OR start.provenance.confidence == "unverified" → emit TEMPORAL-METADATA-MISSING and continue (start is required for any anachronism direction).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).start.provenance.confidence ∈ {high, medium} → proceed to step 3.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.effective_date_range.start > event_date.end → emit TEMPORAL-ANACHRONISTIC-CITATION with rationale "cited version postdates event".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".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:
<!--ref:slug--> marker appears in the same sentence, look up that slug's version_family_id in timeline.yaml.<!--ref:slug--> appears in the same paragraph, use that slug's version_family_id.\b(19|20)\d{2}\b match; if absent, emit no finding.<year> capture is the comparator year.timeline.yaml for a source entry with version_family_id == <bound> AND published_date.value parses to year == <comparator_year>.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.
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:
<!--ref:slug--> marker OR date capture closest preceding the trigger phrase, within the same sentence<!--ref:slug--> marker OR date capture closest following the trigger phrase, within the same sentenceIf 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.
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):
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:
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.
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.
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:
verification_method ∈ crossref_and_pdftotext | crossref_only | pdftotext_only | noneconfidence ∈ high | medium | low | conflictProcess (timeline_extraction_agent runs at ingest time):
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.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.confidence per the agreement table below.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:
<!--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.
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.
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:
timeline_extraction_agent processes "Handbook 2024 Edition" (corpus entry handbook-2024ed), it sets version_family_id: handbook-family in the timeline source entry.handbook-2020ed), version_family_id: handbook-family matches, and supersedes: handbook-2020ed is recorded.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").
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) — unchangedclaim_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) — unchangedThis is the critical invariant: temporal verification is an independent sidecar layer, not a deformation of v3.7.3 / v3.8 / v3.9.0 contracts.
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):
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.
| 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 |
New lint: scripts/check_v3_9_4_temporal_verification.py. Validates:
timeline.yaml schema conformance against timeline.schema.json (aggregate, with per-entry shapes in $defs).supersedes/superseded_by).precision and provenance.method.citation_provenance.yaml schema conformance against citation_provenance.schema.json.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.deep-research/agents/report_compiler_agent.md and academic-paper/agents/draft_writer_agent.md (verbatim string match against the §3.3 block content).deep-research/agents/timeline_extraction_agent.md (NOT bibliography_agent — F2 boundary-discipline closure).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):
"deep-research/agents/timeline_extraction_agent.md" to the hardcoded BUCKET_A_AGENTS list. Bucket A count rises from 22 to 23.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.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.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.
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.
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:
doi, calls Crossref API to populate published_date in timeline.yaml.source_pointer, runs pdftotext for first-line scan.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.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.)
This spec is shipping under writing-plans skill discipline. Estimated effort: 4-6 working days for impl phase after spec ships:
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 §8scripts/temporal_integrity_audit.py (the §3.2 5-pass verifier) + golden testsdeep-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).scripts/bootstrap_timeline_yaml.py (Crossref + pdftotext)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.
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.
MODE_REGISTRY.md ARS version from 3.9.3 to 3.9.4.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.v3.9.4 after PR merges and 0-finding codex+gemini dual-track PR review.docs/design/2026-05-12-ars-v3.7.3-claim-faithfulness-and-contaminated-source-spec.md (advisory measurement precedent; contamination_signals + finalizer matrix)docs/design/2026-05-15-issue-103-claim-alignment-audit-spec.md (cite-time finalizer Phase 4 → 5 slot + sibling-aggregate split discipline)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)docs/design/2026-05-18-ars-v3.9.2-phase-boundary-spec.md (Phase 2 single-phase agent discipline)