Skip to content

feat(core): add temporal qualifiers and time-aware search (SPEC-82) - #1426

Open
phernandez wants to merge 1 commit into
posix-meta-predicatesfrom
spec-82-temporal
Open

feat(core): add temporal qualifiers and time-aware search (SPEC-82)#1426
phernandez wants to merge 1 commit into
posix-meta-predicatesfrom
spec-82-temporal

Conversation

@phernandez

Copy link
Copy Markdown
Member

What

SPEC-82 Phases 1+2. Observations may carry one authored temporal qualifier saying when a claim applies in the world, and search gains explicit valid-time filtering over it. Recorded-time history is deliberately not implemented — it depends on SPEC-59 stable identity, and a half-version would make false historical claims.

- [decision] @effective:2026-06-10 The cache layer will use Redis.
- [decision] @effective:2026-07-27 The cache layer will use Memcached; Redis dropped.

search("cache layer", role=effective, valid_at=2026-07-28) returns only Memcached; at 2026-07-01 only Redis; with no temporal filter both compete under ordinary relevance, exactly as today.

Authored forms

@[<role>]<range-literal>    @effective[2026-06-10,2026-07-27)   @[2026-06-10,)
@[<role>:]<point>           @effective:2026-07-27               @2026-07-27

Role is optional in both; omitted, it files on valid. The bracket form needs no separator (no role name starts with [/(); the point form needs : because a date can begin with a letter (yesterday).

Accepts whatever dateparser reads — already a dependency, already used for frontmatter dates at entity_parser.py:198. Precision is the meaning: a year is the year, a month is the month, a date is from that date onward.

Decisions that changed during the build

Three product calls landed mid-implementation and were applied afterward (decision note):

  • A naive timestamp is UTC, not an error. The spec required explicit offsets; the rest of Basic Memory already coerces naive datetimes to UTC (utils.py, recent_activity.py). A rule enforced by one feature and nowhere else is an inconsistency, not a principle. Replaces acceptance criterion 10 with its inverse.
  • Authored form is a date, not a range literal. People date things; they do not write interval notation. Both forms canonicalize to the same stored range, so storage and the dual-dialect contract are untouched. The MVP is agent-facing and the docs say so.
  • One diagnostic only — unknown role. Direct file editing is a supported path and does not deserve format warnings: a token either reads as time or stays ordinary content, silently.
  • New date_order config (YMD default) so slash-formatted dates resolve by documented preference rather than silent guess.

Storage

Portable projection table with scalar bounds and inclusivity flags, so SQLite and PostgreSQL share one logical contract and pass the same containment, overlap, inclusivity, unbounded, and empty-range tests. Rows rebuild on every index pass and die with the entity — a derived projection, never canonical. Native PostgreSQL range columns remain a later optimization generated from these columns.

Guardrails worth noting

  • Date-only never acquires a time of day. Calendar dates and instants stay different kinds; a date query cannot match an instant range or the reverse.
  • A role-less point must start with a digit and be ≥4 characters. dateparser reads may as May and v2 as February; a bare @word at the head of an observation is a mention or a version, and accepting it would file wrong valid time onto ordinary prose. Relative dates still work with an explicit role (@occurred:yesterday).
  • Undated notes are unchanged with no filter, and excluded when a valid-time filter is present.
  • No ranking change. No implicit recency boost anywhere.

Verification

ruff, format, ty — clean. Full unit suite 6,222 passed, 41 skipped (real captured exit codes). 100% coverage on temporal.py (178/178), temporal_qualifier.py (53/53), temporal_filters.py (38/38). The migration docstring's claim that full-text always drives temporal queries was corrected — has_temporal_filter() counts as criteria, so temporal-only searches are valid.

Open question for review

Point precision currently means: year → the year, month → the month, date → from that date onward. That is right for effective/valid (a state that begins and persists) but arguably wrong for occurred/due (an event at a time) — @occurred:2026-06-10 currently means "occurred from June 10 onward," so a query for June 20 matches it. Making the interpretation role-dependent (state roles open-ended, event roles bounded by the written precision) would resolve it. Flagged rather than decided, and it matters for the Moby Dick eval, which authors @occurred heavily.

🤖 Generated with Claude Code

https://claude.ai/code/session_014pmKq6bqCi6Zp6BTHuZjrp

Observations may carry one authored temporal qualifier naming when a claim
applies in the world, and search gains explicit valid-time filtering over
it. Valid time and recorded time stay separate axes; recorded history waits
for SPEC-59 stable identity rather than shipping a half-version that makes
false historical claims.

Authored forms accept what dateparser reads, canonicalized into one portable
range model:

    @effective[2026-06-10,2026-07-27)   range literal, agents and precision
    @effective:2026-07-27               point, role named
    @2026-07-27                         point, files on valid

A naive timestamp is read as UTC, matching the house convention rather than
enforcing an offset rule that exists nowhere else in the system. Date-only
input never acquires a time of day. Slash-formatted dates resolve under a new
date_order config setting. The only diagnostic is an unknown role: everything
else either reads as time or stays ordinary content, silently, because direct
file editing is a supported path and does not deserve warnings.

Storage is a portable projection table with scalar bounds, so SQLite and
Postgres share one logical contract and pass the same containment, overlap,
inclusivity, unbounded, and empty-range tests. Rows rebuild on every index
pass and die with the entity. Undated notes are unchanged when no temporal
filter is present, and excluded when one is.

Closes the Phase 1 and Phase 2 MVP of SPEC-82.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pmKq6bqCi6Zp6BTHuZjrp
Signed-off-by: phernandez <paul@basicmachines.co>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T05:43:23.761741Z 417a355 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 417a355fe4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +195 to +198
Unlike PostgreSQL's `daterange`, a discrete date range is not rewritten into the
canonical `[)` form -- `[a,b]` keeps the inclusivity the author wrote. Evaluating
the authored flags directly is set-equivalent for containment and overlap and needs
no date arithmetic; only the rendered literal differs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize discrete date ranges before overlap checks

For date ranges, preserving the authored bounds is not set-equivalent for overlap because calendar dates form a discrete domain. For example, (2026-01-01,2026-01-03) contains only January 2, while (2026-01-02,2026-01-04) contains only January 3, yet the scalar predicate reports them as overlapping because each raw endpoint lies inside the other's bounds. Normalize date bounds to an equivalent half-open interval using day arithmetic, or make the overlap predicate account for the successor date, so valid_overlaps does not return sources sharing no calendar date.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant