feat(relations): bi-temporal curation — read filter + retire/revalidate (#653) - #682
Open
norrietaylor wants to merge 1 commit into
Open
feat(relations): bi-temporal curation — read filter + retire/revalidate (#653)#682norrietaylor wants to merge 1 commit into
norrietaylor wants to merge 1 commit into
Conversation
…te (#653) Implements epic #653 graph-maintenance #4 (curation). The crux: invalid_at was write-only — no read path honored it, so "retiring" an edge was a no-op. This makes the bi-temporal window actually govern reads. - store: get_related and list_relations exclude soft-retired edges by default (invalid_at in the past, or valid_at in the future) via a shared _RELATION_LIVE_CLAUSE; pass include_retired=True to surface them. Because the metrics subgraph and BFS traversal both build on these reads, retired edges drop out of metrics/traverse automatically. - store: new retire_relation(relation_id, invalid_at=now) soft-retire (idempotent, preferred over the hard remove) and revalidate_relation(relation_id) to clear invalid_at (add_relation's upsert cannot reset it to NULL). - mcp: distillery_relations action="retire" / "revalidate"; the "get" action gains an include_retired passthrough. Deferred to a follow-up: auto-retiring an entry's edges when it is superseded/corrected, and scheduled threshold-based retirement. Tests: retired edge excluded from get_related/list_relations/traverse, surfaced with include_retired, revalidate restores; future invalid_at stays live; retire idempotent + missing-id; MCP retire/revalidate roundtrip. Two existing attribute-round-trip tests updated to read with include_retired (their fixtures set a past invalid_at). Full suite 3215 passed; mypy --strict and ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Phase 3 of finishing epic #653 — graph-maintenance #4 (curation).
Crux finding:
invalid_atwas write-only. No read path (get_related,list_relations, the metrics subgraph, traversal) filtered on it, so soft-retiring an edge was a complete no-op. This PR makes the bi-temporal window actually govern reads.Changes
storeread filter —get_relatedandlist_relationsexclude soft-retired edges by default via a shared_RELATION_LIVE_CLAUSE(invalid_atnull/future andvalid_atnull/past). Passinclude_retired=Trueto surface them. Because the metrics subgraph (list_relations) and BFS traversal (get_related) build on these, retired edges drop out of metrics/traverse automatically — no separate change needed.storeretire/revalidate —retire_relation(relation_id, invalid_at=now)soft-retires (idempotent; preferred over the hardremove), andrevalidate_relation(relation_id)clearsinvalid_at(theadd_relationupsert cannot reset it to NULL, so this is the supported un-retire path).mcp—distillery_relations action="retire"/"revalidate"; thegetaction gains aninclude_retiredpassthrough.Safe by construction
Every existing edge has
invalid_at IS NULL, so the new filter changes nothing until something is explicitly retired.Deferred to a follow-up PR (tracked)
Auto-retiring an entry's edges when it is
supersedes/corrects-ed (cascade), and scheduled threshold-based retirement. The primitives (retire_relation+ read filter) land here; the policy layer follows.Testing
get_related/list_relations/ traversal; surfaced withinclude_retired;revalidaterestores it.invalid_atstays live (bi-temporal correctness); retire idempotent + missing-id returns False; MCP retire/revalidate roundtrip + validation.include_retired(their fixtures set a pastinvalid_at, so the edge is legitimately retired now).mypy --strict src/clean;ruff check src/ tests/clean.🤖 Generated with Claude Code