Skip to content

A document's date reaches the facts that cite it (#987) - #988

Closed
WaylandYang wants to merge 8 commits into
devfrom
fix/attested-from-document-date
Closed

WaylandYang wants to merge 8 commits into
devfrom
fix/attested-from-document-date

Conversation

@WaylandYang

@WaylandYang WaylandYang commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Found while reproducing #987 (every relation of an entity reads "undated" on a base built from dated reports).

Reproduction on dev. A Chinese report whose first lines state 提报日期:2026/09/04, uploaded and extracted:

step result
time words inside sentences resolved, four of four at grade A
the document's own date read: doc_time = 2026-09-04, source content
the facts' attested_from 2026-09-28 04:26, the moment the document was processed

Cause. Extraction runs before dating. A statement is written while the document has no date, so attested_from takes now(). When the resolver later reads the date from the text, set_content_date wrote it on the document and nowhere else. Reads take the lower bound of a fact with no start from COALESCE(valid_from, attested_from), so a report from 2020 uploaded today has its facts read as known since today: the upload time entering world time, which #714 says must not happen.

Fix.

  • documents::set_content_date does it in one transaction: writes the date on the document and on the version the evidence sits on (Per-object document updates lose observation-time ordering during reconciliation #900), locks the timelines of the facts that cite the document, moves their anchors to the earliest dated evidence, carries typed rows along with the statement they came from, and recomputes those timelines (the engine orders start-less rows by evidence date). Anchors only move earlier, the rule attest_earlier already keeps; an ending said without a date (attested_to) moves the same way. The dated-evidence expression is the one temporal::DATED_AT uses.
  • Migration 0100 does the same for rows already written (constant 90).
  • The resolver logs how many anchors moved and emits a graph event.

After the fix, the same reproduction: every fact of the report is attested from 2026-09-04.

Tests. a_document_date_reaches_its_facts: the statement and the typed row it became move to the document's date; an anchor already earlier stays; a fact of another document is not touched; reading the same date again moves nothing. Store suite, server time tests and clippy pass.

Not in this PR. The entity panel shows "undated" for a fact with no start even when it has an attested date; showing that date is the second half of #987 and comes separately.

Refs #987.

🤖 Generated with Claude Code

WaylandYang and others added 5 commits September 28, 2026 12:40
#987: every relation of an entity read "undated" on a base built from
dated reports. Reproduced on dev with a report whose first lines state
2026/09/04: time words inside sentences resolve (four of four, grade A),
the document's own date is read (doc_time 2026-09-04, source content),
and every fact is attested from 2026-09-28, the moment the document was
processed.

Extraction runs before dating. A statement is written while the document
has no date yet, so `attested_from` takes now(); when the resolver later
reads the date from the text it wrote it on the document and nowhere
else. Reads take the lower bound of a fact with no start from
`COALESCE(valid_from, attested_from)`, so a report from 2020 uploaded
today had its facts read as known since today. That is the upload time
entering world time, which #714 says must not happen.

`documents::set_content_date` now does it in one transaction: writes the
date on the document and on the version the evidence sits on (#900),
locks the timelines of the facts that cite the document, moves their
anchors to the earliest dated evidence (only earlier, the rule
`attest_earlier` already keeps; the same for an ending said without a
date), carries typed rows along with the statement they were
materialised from, and recomputes those timelines, since the engine
orders start-less rows by evidence date. Migration 0099 does the same for
rows already written.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
dev reached 90 migrations with #986; this PR adds one more and the CLI test compares the constant
with the number of files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
@WaylandYang

Copy link
Copy Markdown
Contributor Author

Reviewed and ran it on a fresh database with dev merged in: store 292, server 522, CLI 13, clippy clean. The fix is the right shape: one transaction, anchors only move earlier, typed rows follow the statement they came from, the timelines are recomputed, and the dated-evidence expression is the one DATED_AT uses.

One commit pushed: the description says constant 90, but dev reached 90 files with #986, so with 0100 the constant is 91. Without it the CLI schema test fails.

For the second half of #987: a typed row merged from several statements follows only from_statement_id here; if its other sources are earlier, typed_fact_sources is where to look.

WaylandYang and others added 3 commits September 28, 2026 12:54
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
@WaylandYang

Copy link
Copy Markdown
Contributor Author

Superseded by ADR 0064 (#989). This moved attested_from from the processing moment to a single document date and backfilled it; 0064 drops the single document date: attestation is null unless the time context gives the statement an as-of, and a fact with no time holds at every moment. The diagnosis stands (extraction runs before dating, COALESCE($7, now()) writes the processing moment) and is carried into 0064 cut 3.

WaylandYang added a commit that referenced this pull request Sep 28, 2026
…one with no time holds at every moment (#995)

* A statement is attested by the date its own section speaks from, and one with no time holds at every moment

ADR 0064 cut 3. Supersedes #988.

An open statement was written with `attested_from = COALESCE(document
date, now())`, and extraction runs before any date is read, so every
statement was attested at the moment its document was processed. Reads
take the lower bound of a row with no start from that column: a report
from 2020 uploaded today had its facts read as known since today, and
the panel could only say "undated" (#987).

- The insert no longer falls back to now(). A statement written by
  extraction has no attestation unless a source system dated the
  document. A statement a person nodded through is still attested at the
  nod: the person is the evidence.
- After extraction, each statement of the document is attested by the
  `now` entry in force at the position of its quote (the rule cut 2 uses
  for relative expressions), with that entry's words: `attested_by` is
  "提报日期 2026年9月4日". Minutes are attested by the meeting's date,
  not the date they were sent. Only earlier, as `attest_earlier`.
- Typed rows have no evidence of their own: their attestation is the
  earliest of the statements they were materialised from, kept in step
  at the end of materialisation and after attesting.
- A row with neither a valid time nor an attestation holds at every
  moment. An event with no date still holds at none (0022); the guard is
  null-safe, a statement has no predicate and its temporal kind is NULL.
  Reasoning reads an unattested premise as open below.
- An ending said without a date needs an anchor by CHECK; with no
  attestation it takes the moment the ledger recorded it.
- Migration 0100: `attested_from` nullable, `attested_by`; open
  statements move to their earliest dated evidence or to NULL, typed rows
  follow their sources.
- The panel says "as of 2026-09-04 · 提报日期 2026年9月4日" for a fact
  with no interval of its own, and "undated" only when there is nothing.

Time bench, three runs: as-of right 0 of 10 before, 9–10 of 10 after; no
sentence without time words was given a start.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* The record names the pull request of cut 3

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

---------

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@WaylandYang
WaylandYang deleted the fix/attested-from-document-date branch October 14, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant