The graph reads rewind the recording axis by WaylandYang · Pull Request #317 · deeplethe/utopia · GitHub
Skip to content

The graph reads rewind the recording axis - #317

Merged
WaylandYang merged 3 commits into
devfrom
feat/the-second-clock-can-be-rewound
Sep 4, 2026
Merged

WaylandYang merged 3 commits into
devfrom
feat/the-second-clock-can-be-rewound

Conversation

@WaylandYang

Copy link
Copy Markdown
Contributor

Part of #307 — the read paths and the API. The graph control is 0019's open question and stays a separate cut, so this does not close the issue.

edges_among carried an unconditional invalidated_at IS NULL, so a fact corrected in March was absent at every slider position. The rows have been on disk since the graph migration; nothing shipped could read them.

One predicate, in one place

crates/utopia-store/src/record_axis.rs renders held_at(T) — recorded_at <= T AND (invalidated_at IS NULL OR invalidated_at > T) — and no read writes that filter by hand any more. The parameter binds Option<DateTime<Utc>>, where NULL means now and the predicate degenerates into the old invalidated_at IS NULL; each read keeps one statement rather than one per case, and a second statement is the next place a WHERE goes missing.

Five tables answer the same question under different column names, and each gets exactly one renderer: facts (recorded_at), derived_facts (derived_at), axiom_violations (detected_at + decided_at), fact_conflicts (created_at + resolved_at), chunks (created_at + superseded_at).

Write paths keep the hard-coded filter, as 0019 specifies: confirm_fact, reject_fact, adoption's undo and the dedup lookups guard the current row, and a correction is never made as of March. Review's queues are "now" by definition and are untouched.

What rewinds

overview, neighborhood, edges_among and entity_detail take as_of beside the existing at, and the two travel to the API as separate query parameters. Folded into one control they would answer "the world in March, as we understand it now" with "the world in March as we understood it then", and both look plausible on screen.

Everything drawn alongside the edges follows the same clock, or the picture contradicts itself: node degree, the edge total behind "150 / 325", the BFS that finds the neighbours, the panel's facts, the contested marks, and stale — a chunk superseded in August was live evidence in March. Ghost edges (0017 §3) rewind through their violation's own two columns rather than being pinned to today's open set.

Node totals do not rewind. Entities have no recording axis — merged_into records that a merge happened, not when — so two entities merged in March are still one node at an as_of in February. Unwinding that means reading entity_merges, which 0019 already names as a second cut.

Verified

the_second_clock_can_be_rewound.rs is database-backed and asserts both directions, since they fail differently: a retracted fact absent at as_of = now (a predicate written backwards revives every invalidated row), present at an as_of before its invalidated_at (a predicate left unconnected reads "now" forever and replays nothing), and a correction recorded on 03-20 absent at 03-10 (writing only the lower bound lets March see April's rewrite). A derived fact with its own two dates is in the fixture, so the derived_facts branch of the UNION is covered rather than assumed.

The whole utopia-store suite passes against a freshly migrated database with UTOPIA_TEST_REQUIRE_DB=1, plus cargo fmt --check and cargo clippy --all-targets, clean.

No browser check: this cut has no UI. The route layer is parse-and-pass, and what it passes is covered above.

🤖 Generated with Claude Code

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