docs/handoff-note-timestamps.md
Rama main · réplica de lectura del repositorio original.
<!-- authority:non-specification -->
<!-- authority:implementation-reference -->
# Handoff — second-granularity timestamps for note knowledge-time
Historical implementation handoff only. OpenSpec is the durable specification
authority; this file preserves execution context from the original work.
Self-contained brief for a fresh session. Everything needed is here; no prior
conversation context is assumed.
## The task
Exomem records note `created` / `updated` as bare calendar dates. Move to
**second-granularity, timezone-aware timestamps going forward**, without
rewriting history.
## Why this matters
Exomem's differentiator is bitemporal reasoning — world time × knowledge time.
It currently records its own **knowledge time to the day**. The recording
granularity sits below the capability being claimed.
Observed directly: a note created and revised twice inside ~2 hours has three
history entries all reading `2026-08-05`, with `created == updated`. The only
ordering signal is array position, not data. Questions like "has this page
changed since I read it?" and "in what order did I reach these conclusions?" are
unanswerable within a day.
Neither test suite catches this. The memory-proof benchmark models knowledge time
in *weeks* (`recorded_week`), so it is structurally blind to sub-day ordering.
## Decisions already made — do not relitigate
1. **Second granularity.** Not milliseconds: false precision for hand-authored
notes, and second-level already orders anything a human or agent does
sequentially.
2. **No backfill.** Existing date-only values stay date-only. Rewriting them as
midnight timestamps would assert a precision that was never captured — it
manufactures data. Old notes genuinely have unknown intra-day time and the
schema should say so.
3. **Mixed format is permanent**, not a migration window. Readers must handle
both indefinitely.
4. **Timezone-aware.** A naive local timestamp is unorderable across machines.
## The design question that actually matters
Because of decision 2, **precision becomes part of the data**, and comparison is
no longer total. A date-only value denotes *an unknown instant within that day*;
a timestamp denotes a specific instant. So:
| a | b | result |
|---|---|---|
| `2026-08-04` | `2026-08-05T09:00:00Z` | a before b — the whole day precedes |
| `2026-08-05` | `2026-08-05T09:00:00Z` | **indeterminate** — cannot be ordered |
| `2026-08-05` | `2026-08-05` | **indeterminate** |
| `2026-08-05T09:00:00Z` | `2026-08-05T11:00:00Z` | a before b |
**Recommendation:** a comparison helper returning four-valued
`before | after | same | indeterminate`, never collapsing an unknown into a
guess. This is the same principle the benchmark's scoring layer already uses —
*unsupported-never-zero*: when the data cannot justify a verdict, report that it
cannot, rather than fabricating one. Sorting UIs may fall back to a stable
secondary key, but the ambiguity should be surfaced, not hidden.
Push back if you disagree, but argue it against the table above.
## Hazards found — audit these, they are the real work
The write path is one line. The value lives in the read and comparison paths.
- **`src/exomem/audit.py:952`** — `dt.date.fromisoformat(value.strip()[:10])`
- **`src/exomem/find_policy.py:236`** — `date.fromisoformat(str(value).strip()[:10])`
Both already *tolerate* a timestamp (no crash) but truncate to 10 characters,
collapsing every comparison back to day granularity. **If these are left as-is
the change is cosmetic** — timestamps get written and then ignored. Sweep for
other `[:10]` / `fromisoformat` sites on frontmatter values.
- **`src/exomem/adoption_run.py:736`** — `str(plan.get("created") or "")[:10]`,
same truncation shape (adoption plans, not notes — check whether it matters).
- **`src/exomem/audit_fix.py:177` `_as_iso_date`** — verified **safe**: strings
pass through unchanged, and `datetime` subclasses `date` so `.isoformat()`
preserves the time component. No change needed, but pin it with a test.
- **`src/exomem/note.py:12`** — production-log filenames derive `YYYY-MM-<slug>`
from `created`. Must keep working when `created` carries a time.
## Files to change
- **Write:** `src/exomem/note.py:1070` (`created:`), `src/exomem/edit.py:213-224`
(`updated:`), `src/exomem/replace.py:99-100`. All currently
`dt.date.today().isoformat()`.
- **Schema/validation:** `src/exomem/audit.py:1115-1121` lists `created`/`updated`
as required for seven note types (`research-note`, `insight`, `failure`,
`pattern`, `experiment`, `production-log`, `entity`). Validation must accept
both forms.
- **History entries** carry the same date-only field — include them.
- Precedent for timestamps already in-tree: `adoption_run.py:627` `_now_iso()`,
`review_state.py:202` timezone-aware `datetime`. Reuse rather than invent.
## Benchmark-side work (same change, do not skip)
The benchmark should be able to *measure* sub-day temporality — otherwise this
capability ships untested and we repeat the mistake that hid the gap.
- Corpus knowledge time is `recorded_week: int` (0..11) in
`benchmarks/membench/schema.py`. Sub-day ordering needs finer recorded time on
`Assertion` / `StatusSpan`.
- Proportionate scope: a family where two records land on the **same day** and
their order determines the correct answer — a system that only knows the day
cannot answer, and must abstain rather than guess.
**Timing constraint, important:** this changes generated corpus bytes. It must
land **before** the packaging lane pins v0.2 release bytes (OpenSpec tasks
4.1/4.2 in `openspec/changes/expand-memory-proof-benchmark/`). Doing it after
means reissuing the release.
## Constraints
- Work in a dedicated worktree off `origin/main`; never the shared primary
checkout. Report the path in the first progress update.
- Conventional-commit messages. **No `Co-authored-by:` trailers for any AI tool
and no AI session URLs** in commits, PR titles/bodies, or release notes.
- Do not push or open PRs without explicit approval.
- Lean test command:
`EXOMEM_DISABLE_EMBEDDINGS=1 uv run pytest tests/ -q --timeout=60`
- Follow repo contribution instructions and OpenSpec conventions; validate any
OpenSpec change with `openspec validate <name> --strict`.
## Acceptance
1. New notes carry second-granularity timezone-aware `created`/`updated`;
existing date-only notes are **unmodified** — assert this explicitly.
2. Comparison helper implemented with the four-valued result, unit-tested against
every row of the table above.
3. Every truncation site above either fixed or shown to be harmless with a test.
4. Round-trip test: write a timestamped note, run the audit and audit-fix paths,
assert the time component survives.
5. Mixed-vault test: a vault containing both formats validates, sorts, and
reports indeterminate ordering where it genuinely exists.
6. Benchmark family added, corpus regenerates deterministically, and a
day-granularity system provably cannot pass it.