Skip to content

feat(temporal): canonical half-open bounds for claim windows (D107 WP-T.0a) - #372

Merged
fazpu merged 4 commits into
mainfrom
feat/canonical-bounds
Sep 3, 2026
Merged

feat(temporal): canonical half-open bounds for claim windows (D107 WP-T.0a)#372
fazpu merged 4 commits into
mainfrom
feat/canonical-bounds

Conversation

@fazpu

@fazpu fazpu commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

A stored D41 claim window is inclusive and carries a precision (day/month/quarter/year/instant/open/unknown), and the engine compared the raw values directly. Audit finding 4.13 (plan/analysis/time_handling_audit.md): a day-precision claim was a zero-width point [2023-05-07 00:00, 2023-05-07 00:00], so an intraday claims_as_of window returned nothing; adjacent units touched without overlapping; D106's _windows_disjoint inherited the same point semantics, so a day-precision claim and an instant inside that day missed each other.

Change — D107 §5, WP-T.0a (engine-internal)

One canonicalisation, two twins pinned equal by tests:

  • core/temporal.pycanonical_bounds(valid_from, valid_until, precision) → [start, end): both ends aligned to the precision unit in UTC (year 2022-01-01…12-31 → [2022-01-01, 2023-01-01); a day whose stored start is noon → the whole calendar day); instant[t, t+1µs) (a non-empty point); open → unbounded; unknown → no interval. inclusive_request(from, to) maps the caller's inclusive window to [from, to+1µs), so (t, t) is a point query, never empty.
  • Migration p9_26_0047 — IMMUTABLE SQL twins claim_canonical_start(valid_from, valid_precision) / claim_canonical_end(valid_from, valid_until, valid_precision) (date_trunc(unit, ts, 'UTC')), plus a partial expression index on claims so the as-of scan stays indexed. Claim storage and its CHECK constraints are unchanged.
  • claims_as_of filters the candidate scan on the canonical functions with to_exclusive.
  • D106 adjudicator_CLAIMS_ASSERTED, _SELECT_EVIDENCE_FOR_OBS and the _BLOCK_ENTITY lateral select canonical bounds; _windows_disjoint is half-open; the verdict prompt renders the last instant inside a window. Adjudicator and obs-flush generations roll (…:canonical-bounds-1).
  • LoCoMo protocol Full-v21 → Full-v22 (result semantics change; precedent D100–D106): protocol/model/README/runner messages/sharding/tests/benchmark design banner/project-status; fingerprint literal recomputed.
  • Plan: WP-T.0 split into T.0a (this PR) and T.0b — query-space exposure (memory_v1.canonical_bounds, the claims_canonical view with catalog fixtures and deletion-matrix cells, the shipped claims_as_of example whose unknown count is structurally zero today). Recorded in plan/plans/temporal_clocks.md and D107 temporal clocks — implementation program (WP-T.0…T.5) and the four gating spikes #364.

Tests

src/tests/core/test_temporal.py (pure table: bounded precisions align both ends incl. year rollover; instant overlaps itself; open unbounded; unknown no interval; adjacent days disjoint; intraday and point requests find the day; a day and an instant inside it overlap; naive inputs read as UTC), src/tests/spine/test_canonical_bounds_sql.py (the SQL twins equal the Python function row by row), batch-B test_claims_as_of_finds_a_day_precision_claim_from_an_intraday_window (intraday window, point query, next-day miss), test_d107_a_day_and_an_instant_inside_it_overlap (adjudication honours evidence for an instant inside the day; the same instant next day is a new occurrence). Existing D43/D88/D90/D106 proofs, E3 fan-out, protocol and runner suites: 148 passed; batch-B and the SQL twin suites green. ruff/pyright clean repo-wide. The operational-scale spike run is in progress at PR-open time and will be reported here.

Contributor agreement

Review

Codex (gpt-5.6-sol, xhigh) review to be posted with a triage. Part of #364.

🤖 Generated with Claude Code

https://claude.ai/code/session_016p9xB9T7VYREuVmohDPKjR

fazpu and others added 2 commits September 3, 2026 18:37
…-T.0a)

A stored D41 window is inclusive and carries a precision, and the engine
compared those raw values directly: a day-precision claim was a zero-width
point that an intraday claims_as_of window missed, adjacent units touched
without overlapping, and D106's disjointness test inherited the same point
semantics.

Add one canonicalisation with two twins that must stay equal: the pure
`core/temporal.py` (`canonical_bounds`, `inclusive_request`, `point_request`)
and IMMUTABLE SQL functions `claim_canonical_start` / `claim_canonical_end`
(migration p9_26_0047, with an expression index so the as-of scan stays
indexed). Both ends align to the precision unit in UTC; an instant is a
non-empty point; an open window has no end; unknown is no interval; the
caller's inclusive `to` becomes an exclusive `to + 1 µs`, so `(t, t)` is a
point query. `claims_as_of` filters on the canonical functions; the D106
block lateral, claim-timing reads and `_windows_disjoint` use the same
half-open bounds, and the prompt renders the last instant inside a window.
Claim storage and its CHECKs are unchanged.

Roll the observation adjudicator and obs-flush generations and the LoCoMo
protocol (Full-v21 -> Full-v22). Tests: the pure table, the SQL twin row by
row, an intraday `claims_as_of` window and a point query, and a day-vs-instant
adjudication proof. WP-T.0 is split into T.0a (this) and T.0b (query-space
exposure) in the plan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9xB9T7VYREuVmohDPKjR
…dent

Codex review of #372: `timestamptz + interval` uses the SESSION time zone's
calendar fields, so `claim_canonical_end` disagreed with its Python twin in a
non-UTC session (a March-1 UTC month boundary read as February 28 in
America/New_York, so "+1 month" gave March 28) and an IMMUTABLE function that
feeds an expression index must not depend on the session. Both functions now
truncate and advance on a UTC-naive timestamp (`AT TIME ZONE 'UTC'` in and
out). The verdict prompt's `_date_text` likewise takes the UTC calendar day
of an aware driver value instead of the session-zone day.

Tests: the SQL twin equals Python under America/New_York, Asia/Kolkata and
Pacific/Auckland across month/day(DST)/year/quarter-with-null-end/instant;
`_date_text` prints the UTC day for a New York-rendered midnight. Benchmark
banner and status page now say the library's `claims_as_of` results changed
while the benchmark's read-tool set did not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9xB9T7VYREuVmohDPKjR
@fazpu

fazpu commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Codex review (gpt-5.6-sol, reasoning xhigh) — round 1, and triage

Antigravity is quota-blocked until ~2026-09-07 ("Individual quota reached"); this PR carries the Codex review only, as #360/#369 did.

Triage

# Severity Finding Verdict Action
1 blocker claim_canonical_end did timestamptz + interval after date_trunc(unit, ts, 'UTC'); PostgreSQL performs that calendar arithmetic in the session TimeZone, so the SQL twin disagreed with Python in non-UTC sessions (March 1 UTC reads as Feb 28 in New York → "+1 month" = March 28) and the IMMUTABLE expression index was unsafe. Accepted Fixed in 47e7ec7: both functions truncate and advance on a UTC-naive timestamp (AT TIME ZONE 'UTC' in and out), which is session-independent; comment in the DDL says why. New test_sql_twin_is_session_timezone_independent runs the twin under SET LOCAL TIME ZONE America/New_York, Asia/Kolkata (+05:30) and Pacific/Auckland across month-boundary, US-DST day, year, quarter-with-NULL-stored-end and instant — 15 cases, all equal to Python.
2 major _date_text took .date() of the driver value, which psycopg returns in the connection's session zone; a canonical day could print as the evening before. Accepted Fixed in 47e7ec7: aware values are converted to UTC before taking the date (naive read as UTC). test_prompt_dates_render_the_utc_calendar_day proves it on a New York-rendered UTC midnight.
3 minor v22 provenance note said claims_as_of semantics changed and then "no retrieval behavior changes"; project-status said "Ingestion provenance only". Accepted Reworded in 47e7ec7: the library's claims_as_of result set changed; the benchmark's exposed read-tool set, retry, model-effort and call-budget behaviour did not.

Verified after the fix: SQL-twin + pure temporal suites 34 passed; adjudication + E3 fan-out 60 passed; LoCoMo benchmark suites 174 passed; operational-scale spike + temporal 20 passed (pre-fix run; the spike does not exercise the functions' session-zone path); ruff/pyright clean.

Full review text
  1. [blocker] src/rememberstack/spine/migrations/versions/p9_26_0047_canonical_bounds.py:50claim_canonical_end is session-timezone-dependent despite being declared IMMUTABLE. The bounded branches add calendar intervals directly to a timestamptz. PostgreSQL performs that arithmetic in the session TimeZone; explicit-UTC date_trunc does not make the subsequent addition UTC-based. For example, under America/New_York, March’s UTC boundary is locally February 28, so adding one month can produce March 28 rather than April 1. This disagrees with Python, can make claims_as_of miss valid dates, and makes the expression index unsafe because indexed values depend on the session that created or updated them. PostgreSQL documents that timestamptz + interval uses local calendar fields and is equivalent to date_add without an explicit zone. PostgreSQL date/time arithmetic Fix every day/month/quarter/year branch using date_add(..., interval ..., 'UTC'), or convert to UTC timestamp, add the interval, then convert back with AT TIME ZONE 'UTC'. Add SQL-equivalence tests after SET LOCAL TIME ZONE to at least one positive- and one negative-offset zone, including DST and bounded-null fallback cases.

  2. [major] src/rememberstack/spine/observation_adjudication.py:1315 — prompt dates are rendered in the database session timezone, not canonical UTC. _date_text calls .date() directly. Psycopg returns timestamptz values in the connection’s configured timezone, so canonical instant 2023-05-07T00:00Z becomes May 6 in America/New_York; a canonical day can render as “May 6 to May 7” instead of “May 7.” This can mislead the model’s same-occurrence/date-dispute judgment. Psycopg datetime adaptation _last_inside correctly recovers the original instant for instant precision, but the recovered datetime must be converted to UTC before extracting its date. Normalize aware datetimes with astimezone(UTC) in _date_text and add non-UTC ZoneInfo tests.

  3. [minor] plan/designs/locomo_benchmark_design.md:15 — the v22 provenance description contradicts itself. It states that claims_as_of retrieval semantics changed, then says no retrieval behavior changed; website/src/app/docs/project-status/page.mdx:258 similarly calls the roll “Ingestion provenance only.” Clarify that the benchmark’s exposed read-tool set is unchanged, while the engine-library claims_as_of behavior did change, and use that wording consistently.

Verified

  • Python canonicalization correctly handles UTC conversion, month/quarter/year rollover, instants, open/unknown windows, and bounded precision with a null stored end.
  • Half-open overlap and inclusive point-request conversion are correct.
  • claims_as_of uses the exact indexed function expressions, preserves raw claim_valid_from ordering, and leaves excluded_unstamped behavior unchanged.
  • D106 callers receive canonical bounds; <= disjointness and None-as-open aggregation are consistent. The nested lateral preserves the current-testimony filter.
  • The migration is the sole Alembic head; downgrade order is complete, and the previous raw index remains available.
  • Adjudicator, observation-flush, Full-v22 protocol, expected component version, sharding defaults, README, status page, and locked fingerprint were mechanically rolled consistently.
  • WP-T.0b omissions are explicitly deferred in the sequencing plan.
  • src/tests/core/test_temporal.py: 10 passed. Ruff lint/format and git diff --check passed. Integration tests were not run as requested.

Merge verdict

Do not merge until findings 1 and 2 are fixed and exercised under non-UTC PostgreSQL session timezones. The remaining canonicalization, query predicate, D106 interaction, migration lifecycle, and version-roll work otherwise looks consistent with WP-T.0a.

…d literals

CI on #372: the two new test files were absent from the CI test inventory;
the migration chain and head literals still ended at p9_25_0046; and the
batch-B intraday test inserted its claim against a random chunk id, so the
history hydration view (claims_visible_history joins chunks) dropped it after
the candidate scan had found it. The test claim now hangs off a live chunk
of the live document, which is what the proof is about.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9xB9T7VYREuVmohDPKjR
@fazpu

fazpu commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

CI round 1 on 47e7ec7 failed on three mechanical points, fixed in 870b0d1: the two new test files were missing from .github/ci/{unit,integration}-paths.txt (Quality lane), the migration chain/head literals in test_migrations.py still ended at p9_25_0046 (Contract smoke + Integration workers), and the batch-B intraday test inserted its claim against a random chunk id, so claims_visible_history (which joins chunks) dropped it at hydration even though the canonical candidate scan had found it — the test claim now hangs off a live chunk of the live document. Batch-B 15/15 locally; migration-chain tests re-running locally and in CI.

The Compose quickstart lane asserts the post-upgrade alembic head literal;
migration p9_26_0047 (canonical bounds) moves it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9xB9T7VYREuVmohDPKjR
@fazpu
fazpu merged commit 43c03aa into main Sep 3, 2026
12 checks passed
@fazpu
fazpu deleted the feat/canonical-bounds branch September 3, 2026 17:47
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