Skip to content

Commit c3ed849

Browse files
fix(graph): honor historical visibility anchors
1 parent acadc4b commit c3ed849

2 files changed

Lines changed: 108 additions & 4 deletions

File tree

engraphis/service.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8530,8 +8530,8 @@ def temporal_ghost(row: Any) -> bool:
85308530
"ON visibility_support.edge_id=visibility_edge.id "
85318531
)
85328532
# A live scene must count only the same temporal edge/support/memory rows
8533-
# that the later edge query can render. History intentionally keeps the
8534-
# broader public relation set so closed rows remain available as ghosts.
8533+
# that the later edge query can render. History keeps closed rows available
8534+
# as ghosts, but still applies the selected world/system-time anchors.
85358535
if not include_history:
85368536
visibility_sql += (
85378537
"AND (visibility_support.valid_from IS NULL "
@@ -8546,6 +8546,16 @@ def temporal_ghost(row: Any) -> bool:
85468546
"OR ?<visibility_support.expired_at) "
85478547
)
85488548
visibility_params.extend((t, t, t, known_t, known_t))
8549+
else:
8550+
visibility_sql += (
8551+
"AND (visibility_support.valid_from IS NULL "
8552+
"OR visibility_support.valid_from<=?) "
8553+
"AND (visibility_support.ingested_at IS NULL "
8554+
"OR visibility_support.ingested_at<=?) "
8555+
"AND (visibility_support.expired_at IS NULL "
8556+
"OR ?<visibility_support.expired_at) "
8557+
)
8558+
visibility_params.extend((t, known_t, known_t))
85498559
visibility_sql += (
85508560
"LEFT JOIN memories visibility_memory "
85518561
"ON visibility_memory.id=visibility_support.memory_id "
@@ -8565,6 +8575,17 @@ def temporal_ghost(row: Any) -> bool:
85658575
"OR ?<visibility_memory.expired_at) "
85668576
)
85678577
visibility_params.extend((wid, t, t, t, known_t, known_t))
8578+
else:
8579+
visibility_sql += (
8580+
"AND visibility_memory.workspace_id=? "
8581+
"AND (visibility_memory.valid_from IS NULL "
8582+
"OR visibility_memory.valid_from<=?) "
8583+
"AND (visibility_memory.ingested_at IS NULL "
8584+
"OR visibility_memory.ingested_at<=?) "
8585+
"AND (visibility_memory.expired_at IS NULL "
8586+
"OR ?<visibility_memory.expired_at) "
8587+
)
8588+
visibility_params.extend((wid, t, known_t, known_t))
85688589
visibility_sql += "WHERE visibility_edge.workspace_id=? "
85698590
visibility_params.append(wid)
85708591
if repo_id:
@@ -8584,6 +8605,16 @@ def temporal_ghost(row: Any) -> bool:
85848605
"OR ?<visibility_edge.expired_at) "
85858606
)
85868607
visibility_params.extend((t, t, t, known_t, known_t))
8608+
else:
8609+
visibility_sql += (
8610+
"AND (visibility_edge.valid_from IS NULL "
8611+
"OR visibility_edge.valid_from<=?) "
8612+
"AND (visibility_edge.ingested_at IS NULL "
8613+
"OR visibility_edge.ingested_at<=?) "
8614+
"AND (visibility_edge.expired_at IS NULL "
8615+
"OR ?<visibility_edge.expired_at) "
8616+
)
8617+
visibility_params.extend((t, known_t, known_t))
85878618
visibility_sql += (
85888619
"GROUP BY visibility_edge.id, visibility_edge.repo_id, "
85898620
"visibility_edge.src, visibility_edge.dst) "
@@ -8779,10 +8810,11 @@ def temporal_ghost(row: Any) -> bool:
87798810
entity_sql += " AND (NOT EXISTS (SELECT 1 FROM edges hidden_edge "
87808811
entity_sql += (
87818812
"WHERE hidden_edge.workspace_id=? "
8782-
"AND (hidden_edge.src=entity.id OR hidden_edge.dst=entity.id)) "
8813+
"AND (hidden_edge.src=entity.id OR hidden_edge.dst=entity.id) "
8814+
"AND (hidden_edge.ingested_at IS NULL OR hidden_edge.ingested_at<=?)) "
87838815
"OR EXISTS (SELECT 1 FROM edges public_edge "
87848816
)
8785-
entity_params.extend((wid,))
8817+
entity_params.extend((wid, known_t))
87868818
if not include_history:
87878819
entity_sql += (
87888820
"WHERE public_edge.workspace_id=? "

tests/test_graph_explorer_v2.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3884,6 +3884,78 @@ def test_live_visibility_cap_ignores_closed_relations(monkeypatch):
38843884
assert {edge["id"] for edge in scene["edges"]} == {"edge_bg"}
38853885

38863886

3887+
def test_history_visibility_cap_ignores_edges_learned_after_known_at(monkeypatch):
3888+
"""Future system-time rows must not consume a time-travel visibility budget."""
3889+
service, alpha, beta, _gamma = _seed_service()
3890+
workspace_id = service.store.get_or_create_workspace("acme")
3891+
old_target = service.store.upsert_entity(Node(
3892+
id="history-old-target", name="History Old Target", ntype="concept",
3893+
workspace_id=workspace_id,
3894+
))
3895+
future_target = service.store.upsert_entity(Node(
3896+
id="history-future-target", name="History Future Target", ntype="concept",
3897+
workspace_id=workspace_id,
3898+
))
3899+
service.store.upsert_edge(Edge(
3900+
id="history-old-edge", src=alpha, dst=old_target, relation="old",
3901+
workspace_id=workspace_id,
3902+
))
3903+
service.store.upsert_edge(Edge(
3904+
id="history-future-edge", src=beta, dst=future_target, relation="future",
3905+
workspace_id=workspace_id,
3906+
))
3907+
service.store.conn.execute("UPDATE entities SET created_at=0")
3908+
service.store.conn.execute(
3909+
"UPDATE edges SET valid_from=0, ingested_at=0 WHERE id='history-old-edge'"
3910+
)
3911+
service.store.conn.execute(
3912+
"UPDATE edges SET valid_from=0, ingested_at=200 WHERE id='history-future-edge'"
3913+
)
3914+
service.store.conn.commit()
3915+
monkeypatch.setattr(service_module, "MAX_GRAPH_ANALYSIS_EDGES", 1)
3916+
3917+
scene = service.graph_scene(
3918+
workspace="acme", level="complete", include_memory_nodes=False,
3919+
include_history=True, valid_at=100, known_at=100,
3920+
)
3921+
3922+
edge_ids = {edge["id"] for edge in scene["edges"]}
3923+
assert "history-old-edge" in edge_ids
3924+
assert "history-future-edge" not in edge_ids
3925+
3926+
3927+
def test_live_scene_keeps_entities_before_future_edge_known_at():
3928+
"""An edge learned after known_at must not hide its otherwise unlinked nodes."""
3929+
service = MemoryService.create(":memory:", graph_extractor="none")
3930+
workspace_id = service.store.get_or_create_workspace("acme")
3931+
source = service.store.upsert_entity(Node(
3932+
id="known-source", name="Known Source", ntype="concept",
3933+
workspace_id=workspace_id,
3934+
))
3935+
target = service.store.upsert_entity(Node(
3936+
id="known-target", name="Known Target", ntype="concept",
3937+
workspace_id=workspace_id,
3938+
))
3939+
service.store.upsert_edge(Edge(
3940+
id="future-edge", src=source, dst=target, relation="future",
3941+
workspace_id=workspace_id,
3942+
))
3943+
service.store.conn.execute("UPDATE entities SET created_at=0")
3944+
service.store.conn.execute(
3945+
"UPDATE edges SET valid_from=0, ingested_at=200 WHERE id='future-edge'"
3946+
)
3947+
service.store.conn.commit()
3948+
3949+
scene = service.graph_scene(
3950+
workspace="acme", level="complete", include_memory_nodes=False,
3951+
valid_at=100, known_at=100,
3952+
)
3953+
3954+
node_ids = {node["id"] for node in scene["nodes"]}
3955+
assert {source, target} <= node_ids
3956+
assert "future-edge" not in {edge["id"] for edge in scene["edges"]}
3957+
3958+
38873959
def test_live_entity_cap_ignores_closed_edge_only_entities(monkeypatch):
38883960
"""Closed-only endpoints must not consume the live entity candidate cap."""
38893961
service, _alpha, _beta, _gamma = _seed_service()

0 commit comments

Comments
 (0)