graph-engine-memory 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graph_engine/__init__.py +1 -0
- graph_engine/__main__.py +1275 -0
- graph_engine/access.py +202 -0
- graph_engine/agent_memory.py +103 -0
- graph_engine/brain.py +731 -0
- graph_engine/brain_engine.py +559 -0
- graph_engine/communities.py +503 -0
- graph_engine/consolidation.py +191 -0
- graph_engine/context.py +203 -0
- graph_engine/decay.py +65 -0
- graph_engine/demo.py +165 -0
- graph_engine/dream.py +408 -0
- graph_engine/embedder.py +84 -0
- graph_engine/engine.py +58 -0
- graph_engine/entities.py +376 -0
- graph_engine/evals.py +1206 -0
- graph_engine/feedback.py +155 -0
- graph_engine/gaps.py +209 -0
- graph_engine/graph.py +133 -0
- graph_engine/hygiene.py +213 -0
- graph_engine/inference.py +251 -0
- graph_engine/intent.py +207 -0
- graph_engine/mcp/__init__.py +1 -0
- graph_engine/mcp/agent/instructions.md +34 -0
- graph_engine/mcp/format.py +66 -0
- graph_engine/mcp/server.py +407 -0
- graph_engine/merge.py +220 -0
- graph_engine/model.py +56 -0
- graph_engine/namespaces.py +74 -0
- graph_engine/recall.py +161 -0
- graph_engine/report.py +401 -0
- graph_engine/reranker.py +86 -0
- graph_engine/retrieval.py +296 -0
- graph_engine/review.py +95 -0
- graph_engine/runtime.py +83 -0
- graph_engine/server.py +239 -0
- graph_engine/similarity.py +42 -0
- graph_engine/store.py +63 -0
- graph_engine/suggester.py +63 -0
- graph_engine/temporal.py +216 -0
- graph_engine/tiers.py +135 -0
- graph_engine/web/app.js +578 -0
- graph_engine/web/index.html +169 -0
- graph_engine/web/report.html +39 -0
- graph_engine/web/report.js +50 -0
- graph_engine/web/review.html +104 -0
- graph_engine/web/review.js +211 -0
- graph_engine_memory-0.6.1.dist-info/METADATA +125 -0
- graph_engine_memory-0.6.1.dist-info/RECORD +53 -0
- graph_engine_memory-0.6.1.dist-info/WHEEL +5 -0
- graph_engine_memory-0.6.1.dist-info/entry_points.txt +3 -0
- graph_engine_memory-0.6.1.dist-info/licenses/LICENSE +21 -0
- graph_engine_memory-0.6.1.dist-info/top_level.txt +1 -0
graph_engine/evals.py
ADDED
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
"""Eval layer for the engine (Roadmap V2#4).
|
|
2
|
+
|
|
3
|
+
End-state verification instead of transcripts: each eval task describes an
|
|
4
|
+
ingest sequence + an ORACLE over the final brain state (does the node exist,
|
|
5
|
+
are the edges set, was the duplicate merged?). Deterministic checkers run
|
|
6
|
+
against the actual brain state — no peeking at intermediate steps.
|
|
7
|
+
|
|
8
|
+
Two tiers (tiered gates):
|
|
9
|
+
- GOLDEN_SET: regression on every engine change. These cases MUST be green
|
|
10
|
+
on the current state — they freeze the as-is behavior as a baseline.
|
|
11
|
+
- ROADMAP_CASES: document desired future behavior from the roadmap
|
|
12
|
+
(confidence bands, intent edges, contradictions). They are registered as
|
|
13
|
+
a specification and turn green once the feature is implemented — making
|
|
14
|
+
progress measurable without breaking the baseline.
|
|
15
|
+
|
|
16
|
+
pass^k: run_eval(... k=...) runs the same task k times against a fresh
|
|
17
|
+
brain and requires ALL runs to pass (protection against flakiness).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Callable
|
|
24
|
+
|
|
25
|
+
from .brain import Brain, Node
|
|
26
|
+
from .brain_engine import BrainEngine
|
|
27
|
+
from .intent import INTENT_KINDS
|
|
28
|
+
from .recall import aggregate as aggregate_recalls
|
|
29
|
+
from .retrieval import explain, retrieve
|
|
30
|
+
from .reranker import ReverseReranker
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Oracle / tasks
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class EdgeExpectation:
|
|
39
|
+
"""An expected edge, text-based (source/target NODE CONTENT).
|
|
40
|
+
|
|
41
|
+
kind="*" means: any active edge between the two nodes suffices
|
|
42
|
+
(robust against exact kind names when only "being connected" matters).
|
|
43
|
+
pending/min_confidence are optional and check the confidence band (V2#3).
|
|
44
|
+
"""
|
|
45
|
+
source: str
|
|
46
|
+
target: str
|
|
47
|
+
kind: str
|
|
48
|
+
pending: bool | None = None # if set: edge must have this pending value
|
|
49
|
+
min_confidence: float | None = None # if set: Edge.confidence >= this value
|
|
50
|
+
# Who created the edge (`roadmap-edge-origin`): "suggester" (cosine kNN),
|
|
51
|
+
# "intent" (marker heuristic), "manual" (ig link / declared relations),
|
|
52
|
+
# "consolidator" (dream pass). Maintenance passes may rewrite heuristic
|
|
53
|
+
# edges; manual ones are user-authored and must be left alone.
|
|
54
|
+
origin: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class RetrievalExpectation:
|
|
59
|
+
"""A retrieval expectation (hybrid dense+BM25): query must return matching nodes."""
|
|
60
|
+
query: str
|
|
61
|
+
top: int = 5
|
|
62
|
+
includes: list[str] = field(default_factory=list) # node texts that must appear in the top-`top`
|
|
63
|
+
excludes: list[str] = field(default_factory=list) # node texts that must NOT appear
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ExplanationExpectation:
|
|
68
|
+
"""Explain a result with reconstructible scores and contextual edges."""
|
|
69
|
+
query: str
|
|
70
|
+
node_text: str
|
|
71
|
+
retrieved: bool = True
|
|
72
|
+
related_texts: list[str] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class CommunityExpectation:
|
|
77
|
+
"""Structural expectation over the community partition of the FINAL graph.
|
|
78
|
+
|
|
79
|
+
Topology evals (report #3): `together` texts must share ONE community,
|
|
80
|
+
`apart` pairs must sit in DIFFERENT communities, `gap` pairs must appear
|
|
81
|
+
together in a reported structural gap. All three are asserted — each one
|
|
82
|
+
alone is trivially green (everything-in-one / everything-singleton /
|
|
83
|
+
report-every-pair). min_size mirrors the CLI default's role: communities
|
|
84
|
+
smaller than this are not gap candidates.
|
|
85
|
+
"""
|
|
86
|
+
together: list[list[str]] = field(default_factory=list)
|
|
87
|
+
apart: list[tuple[str, str]] = field(default_factory=list)
|
|
88
|
+
gap: list[tuple[str, str]] = field(default_factory=list)
|
|
89
|
+
min_size: int = 2
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class NeighborhoodExpectation:
|
|
94
|
+
"""Graph-traversal expectation (report #1): the node with `node_text`
|
|
95
|
+
must have each of `expected_neighbor_texts` within `hops` undirected
|
|
96
|
+
live-edge hops. `absent_neighbor_texts` must NOT be reachable within
|
|
97
|
+
`hops` — an expectation family alone is trivially green, so both run
|
|
98
|
+
together (same rule as CommunityExpectation).
|
|
99
|
+
"""
|
|
100
|
+
node_text: str
|
|
101
|
+
expected_neighbor_texts: list[str] = field(default_factory=list)
|
|
102
|
+
absent_neighbor_texts: list[str] = field(default_factory=list)
|
|
103
|
+
hops: int = 1
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class EvalOracle:
|
|
108
|
+
"""Desired brain state after the ingest sequence."""
|
|
109
|
+
node_count: int | None = None
|
|
110
|
+
nodes_present: list[str] = field(default_factory=list)
|
|
111
|
+
node_absent: list[str] = field(default_factory=list)
|
|
112
|
+
# (text, required sources): the node with this text must have all these sources.
|
|
113
|
+
duplicate_merged: list[tuple[str, list[str]]] = field(default_factory=list)
|
|
114
|
+
edges: list[EdgeExpectation] = field(default_factory=list)
|
|
115
|
+
no_edge: list[EdgeExpectation] = field(default_factory=list)
|
|
116
|
+
retrieval: list[RetrievalExpectation] = field(default_factory=list)
|
|
117
|
+
explanations: list[ExplanationExpectation] = field(default_factory=list)
|
|
118
|
+
# text -> expected node status (V2#2 memory hygiene)
|
|
119
|
+
node_status: dict[str, str] = field(default_factory=dict)
|
|
120
|
+
# topology expectations (report #3): partition-level assertions
|
|
121
|
+
communities: list[CommunityExpectation] = field(default_factory=list)
|
|
122
|
+
# rendered-output expectations (report #7): strings that must appear in /
|
|
123
|
+
# must not appear in the generated BRAIN_REPORT digest
|
|
124
|
+
report_contains: list[str] = field(default_factory=list)
|
|
125
|
+
report_absent: list[str] = field(default_factory=list)
|
|
126
|
+
# graph-traversal expectations (report #1): neighborhood reachability
|
|
127
|
+
neighborhoods: list[NeighborhoodExpectation] = field(default_factory=list)
|
|
128
|
+
# Intent fan-out dam (`roadmap-intent-fanout-cap`): no source node may hold
|
|
129
|
+
# more than this many AUTO-ACCEPTED (pending=False) intent edges. Intent
|
|
130
|
+
# edges carry no confidence, so the confidence bands cannot judge them and
|
|
131
|
+
# the marker heuristic is the only producer — the cap is the structural
|
|
132
|
+
# bound on that stream.
|
|
133
|
+
max_auto_intent_per_source: int | None = None
|
|
134
|
+
# Recall tracking (`roadmap-recall-tracking`): text -> expected
|
|
135
|
+
# `recall_count` after the recall ledger was aggregated. This is the input
|
|
136
|
+
# signal for promotion/decay — a memory that does not know what it is asked
|
|
137
|
+
# for cannot decide what to keep.
|
|
138
|
+
node_recalls: dict[str, int] = field(default_factory=dict)
|
|
139
|
+
# Edge-provenance floors (`roadmap-dream-distill`): origin -> minimum number
|
|
140
|
+
# of LIVE edges carrying that origin. A pass that writes `consolidator` edges
|
|
141
|
+
# is only observable through counts — the texts it generates are not
|
|
142
|
+
# knowable up front. Pending/rejected/invalidated edges do not count.
|
|
143
|
+
min_edges_by_origin: dict[str, int] = field(default_factory=dict)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class EvalTask:
|
|
148
|
+
id: str
|
|
149
|
+
name: str
|
|
150
|
+
ingests: list[tuple[str, dict]] # (text, kwargs) — order is part of the scenario
|
|
151
|
+
oracle: EvalOracle
|
|
152
|
+
# Optional actions AFTER the ingests (e.g. manual same_as links,
|
|
153
|
+
# consolidation, demotion). Return values are ignored.
|
|
154
|
+
actions: list[Callable[[BrainEngine], object]] = field(default_factory=list)
|
|
155
|
+
# Optional reranker (V2#1): set after engine construction so retrieval
|
|
156
|
+
# evals can measure the cross-encoder rerank pass with a deterministic
|
|
157
|
+
# stand-in. None = engine default (no rerank).
|
|
158
|
+
reranker: object | None = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class EvalResult:
|
|
163
|
+
task_id: str
|
|
164
|
+
name: str
|
|
165
|
+
passed: bool
|
|
166
|
+
failures: list[str]
|
|
167
|
+
runs: int
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# End-State-Checker
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def _norm(text: str) -> str:
|
|
175
|
+
return " ".join(text.lower().split())
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def find_node_by_text(brain: Brain, text: str) -> Node | None:
|
|
179
|
+
"""Resolve an oracle text to a node: exact normalized match wins.
|
|
180
|
+
|
|
181
|
+
Prefix fallback exists because memory evolution appends an auto-accepted
|
|
182
|
+
"[evolved …]" cross-reference to the text — the original text stays a
|
|
183
|
+
prefix and must still resolve. Audit #27: when one oracle text is a
|
|
184
|
+
prefix of another, first-hit-wins prefix matching resolved to the wrong
|
|
185
|
+
node — an exact match now always beats a prefix match, and prefix
|
|
186
|
+
matching prefers the SHORTEST prefix (the original, pre-evolution text).
|
|
187
|
+
"""
|
|
188
|
+
target = _norm(text)
|
|
189
|
+
nodes = brain.read_nodes()
|
|
190
|
+
for n in nodes:
|
|
191
|
+
if _norm(n.text) == target:
|
|
192
|
+
return n
|
|
193
|
+
best: Node | None = None
|
|
194
|
+
for n in nodes:
|
|
195
|
+
if _norm(n.text).startswith(target):
|
|
196
|
+
if best is None or len(_norm(n.text)) < len(_norm(best.text)):
|
|
197
|
+
best = n
|
|
198
|
+
return best
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def verify_end_state(brain: Brain, oracle: EvalOracle) -> list[str]:
|
|
202
|
+
"""Check the final brain state against the oracle. Returns all failures ([] = green)."""
|
|
203
|
+
failures: list[str] = []
|
|
204
|
+
nodes = brain.read_nodes()
|
|
205
|
+
edges = brain.read_edges()
|
|
206
|
+
|
|
207
|
+
if oracle.node_count is not None and len(nodes) != oracle.node_count:
|
|
208
|
+
failures.append(f"node_count: expected {oracle.node_count}, got {len(nodes)}")
|
|
209
|
+
|
|
210
|
+
for text in oracle.nodes_present:
|
|
211
|
+
if find_node_by_text(brain, text) is None:
|
|
212
|
+
failures.append(f"node missing: {text!r}")
|
|
213
|
+
|
|
214
|
+
for text in oracle.node_absent:
|
|
215
|
+
if find_node_by_text(brain, text) is not None:
|
|
216
|
+
failures.append(f"node should be absent: {text!r}")
|
|
217
|
+
|
|
218
|
+
for text, req_sources in oracle.duplicate_merged:
|
|
219
|
+
n = find_node_by_text(brain, text)
|
|
220
|
+
if n is None:
|
|
221
|
+
failures.append(f"dup node missing: {text!r}")
|
|
222
|
+
else:
|
|
223
|
+
for s in req_sources:
|
|
224
|
+
if s not in n.sources:
|
|
225
|
+
failures.append(f"node {text!r} missing source {s!r} (got {n.sources})")
|
|
226
|
+
|
|
227
|
+
for text, expected_status in oracle.node_status.items():
|
|
228
|
+
n = find_node_by_text(brain, text)
|
|
229
|
+
if n is None:
|
|
230
|
+
failures.append(f"status node missing: {text!r}")
|
|
231
|
+
elif n.status != expected_status:
|
|
232
|
+
failures.append(f"node {text!r}: expected status {expected_status!r}, got {n.status!r}")
|
|
233
|
+
|
|
234
|
+
for eexp in oracle.edges:
|
|
235
|
+
s = find_node_by_text(brain, eexp.source)
|
|
236
|
+
t = find_node_by_text(brain, eexp.target)
|
|
237
|
+
if s is None or t is None:
|
|
238
|
+
failures.append(f"edge endpoint missing: {eexp.source!r}->{eexp.target!r}")
|
|
239
|
+
continue
|
|
240
|
+
# Direction-agnostic: auto edges point from the newer to the older node,
|
|
241
|
+
# so direction doesn't matter for "being connected".
|
|
242
|
+
active = [
|
|
243
|
+
e for e in edges
|
|
244
|
+
if e.is_current
|
|
245
|
+
and ((e.source == s.id and e.target == t.id) or (e.source == t.id and e.target == s.id))
|
|
246
|
+
]
|
|
247
|
+
if eexp.kind != "*":
|
|
248
|
+
kind_match = [e for e in active if e.kind == eexp.kind]
|
|
249
|
+
else:
|
|
250
|
+
kind_match = active
|
|
251
|
+
if not kind_match:
|
|
252
|
+
failures.append(f"edge missing: {eexp.source!r} --[{eexp.kind}]--> {eexp.target!r}")
|
|
253
|
+
continue
|
|
254
|
+
if eexp.pending is not None and not any(e.pending == eexp.pending for e in kind_match):
|
|
255
|
+
failures.append(
|
|
256
|
+
f"edge {eexp.source!r}->{eexp.target!r}: expected pending={eexp.pending}, "
|
|
257
|
+
f"got {[e.pending for e in kind_match]}"
|
|
258
|
+
)
|
|
259
|
+
if eexp.min_confidence is not None and not any(
|
|
260
|
+
(e.confidence or 0.0) >= eexp.min_confidence for e in kind_match
|
|
261
|
+
):
|
|
262
|
+
failures.append(
|
|
263
|
+
f"edge {eexp.source!r}->{eexp.target!r}: expected confidence>={eexp.min_confidence}, "
|
|
264
|
+
f"got {[e.confidence for e in kind_match]}"
|
|
265
|
+
)
|
|
266
|
+
# Edge provenance (`roadmap-edge-origin`): who CREATED the edge. A
|
|
267
|
+
# maintenance pass may rewrite heuristic edges but never manual ones,
|
|
268
|
+
# so the origin has to be asserted, not inferred from the text.
|
|
269
|
+
if eexp.origin is not None and not any(
|
|
270
|
+
getattr(e, "origin", None) == eexp.origin for e in kind_match
|
|
271
|
+
):
|
|
272
|
+
failures.append(
|
|
273
|
+
f"edge {eexp.source!r}->{eexp.target!r}: expected origin "
|
|
274
|
+
f"{eexp.origin!r}, got {[getattr(e, 'origin', None) for e in kind_match]}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
for eexp in oracle.no_edge:
|
|
278
|
+
s = find_node_by_text(brain, eexp.source)
|
|
279
|
+
t = find_node_by_text(brain, eexp.target)
|
|
280
|
+
if s is None or t is None:
|
|
281
|
+
continue
|
|
282
|
+
# Audit #27: mirror the positive path — only ACTIVE edges count, and
|
|
283
|
+
# kind="*" means any kind; a specific kind must match exactly. An
|
|
284
|
+
# invalidated edge or a different-kind edge is NOT a violation.
|
|
285
|
+
violating = [
|
|
286
|
+
e for e in edges
|
|
287
|
+
if e.is_current
|
|
288
|
+
and ((e.source == s.id and e.target == t.id) or (e.source == t.id and e.target == s.id))
|
|
289
|
+
and (eexp.kind == "*" or e.kind == eexp.kind)
|
|
290
|
+
]
|
|
291
|
+
if violating:
|
|
292
|
+
failures.append(f"unexpected edge: {eexp.source!r} --[{eexp.kind}]--> {eexp.target!r}")
|
|
293
|
+
|
|
294
|
+
if oracle.report_contains or oracle.report_absent:
|
|
295
|
+
from .report import render_report
|
|
296
|
+
rendered = render_report(brain)
|
|
297
|
+
for needle in oracle.report_contains:
|
|
298
|
+
if needle not in rendered:
|
|
299
|
+
failures.append(f"report missing: {needle!r}")
|
|
300
|
+
for needle in oracle.report_absent:
|
|
301
|
+
if needle in rendered:
|
|
302
|
+
failures.append(f"report should not contain: {needle!r}")
|
|
303
|
+
|
|
304
|
+
if oracle.node_recalls:
|
|
305
|
+
for text, expected in oracle.node_recalls.items():
|
|
306
|
+
n = find_node_by_text(brain, text)
|
|
307
|
+
if n is None:
|
|
308
|
+
failures.append(f"recall node missing: {text!r}")
|
|
309
|
+
elif getattr(n, "recall_count", 0) != expected:
|
|
310
|
+
failures.append(
|
|
311
|
+
f"node {text!r}: expected recall_count {expected}, "
|
|
312
|
+
f"got {getattr(n, 'recall_count', 0)}")
|
|
313
|
+
|
|
314
|
+
if oracle.max_auto_intent_per_source is not None:
|
|
315
|
+
per_source: dict[str, int] = {}
|
|
316
|
+
for e in edges:
|
|
317
|
+
if (e.kind in INTENT_KINDS and not e.pending and not e.rejected
|
|
318
|
+
and e.is_current):
|
|
319
|
+
per_source[e.source] = per_source.get(e.source, 0) + 1
|
|
320
|
+
over = {s: n for s, n in per_source.items()
|
|
321
|
+
if n > oracle.max_auto_intent_per_source}
|
|
322
|
+
if over:
|
|
323
|
+
worst_source, worst_n = max(over.items(), key=lambda kv: kv[1])
|
|
324
|
+
failures.append(
|
|
325
|
+
f"intent fan-out: source {worst_source} has {worst_n} auto-accepted "
|
|
326
|
+
f"intent edges (max {oracle.max_auto_intent_per_source}); "
|
|
327
|
+
f"{len(over)} source(s) over the cap")
|
|
328
|
+
|
|
329
|
+
for origin, minimum in oracle.min_edges_by_origin.items():
|
|
330
|
+
found = sum(1 for e in edges
|
|
331
|
+
if getattr(e, "origin", None) == origin
|
|
332
|
+
and not e.pending and not e.rejected and e.is_current)
|
|
333
|
+
if found < minimum:
|
|
334
|
+
failures.append(
|
|
335
|
+
f"edges with origin {origin!r}: expected >= {minimum} live, got {found}")
|
|
336
|
+
|
|
337
|
+
return failures
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def verify_retrieval(engine: BrainEngine, expectations: list[RetrievalExpectation]) -> list[str]:
|
|
341
|
+
"""Check hybrid retrieval: query must return expected nodes in the top-k (or exclude them)."""
|
|
342
|
+
failures: list[str] = []
|
|
343
|
+
id2node = {n.id: n for n in engine.brain.read_nodes()}
|
|
344
|
+
for exp in expectations:
|
|
345
|
+
hits = retrieve(engine, exp.query, k=exp.top)
|
|
346
|
+
hit_texts = [_norm(id2node[nid].text) for nid, _ in hits if nid in id2node]
|
|
347
|
+
for want in exp.includes:
|
|
348
|
+
if not any(_norm(want) in t or t.startswith(_norm(want)) for t in hit_texts):
|
|
349
|
+
failures.append(f"retrieval '{exp.query}' should hit {want!r} in top-{exp.top}")
|
|
350
|
+
for avoid in exp.excludes:
|
|
351
|
+
if any(_norm(avoid) in t or t.startswith(_norm(avoid)) for t in hit_texts):
|
|
352
|
+
failures.append(f"retrieval '{exp.query}' should NOT hit {avoid!r} in top-{exp.top}")
|
|
353
|
+
return failures
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def verify_explanations(engine: BrainEngine,
|
|
357
|
+
expectations: list[ExplanationExpectation]) -> list[str]:
|
|
358
|
+
"""Require score fidelity and expected graph context in the final brain."""
|
|
359
|
+
import math
|
|
360
|
+
|
|
361
|
+
failures: list[str] = []
|
|
362
|
+
for exp in expectations:
|
|
363
|
+
node = find_node_by_text(engine.brain, exp.node_text)
|
|
364
|
+
if node is None:
|
|
365
|
+
failures.append(f"explanation node missing: {exp.node_text!r}")
|
|
366
|
+
continue
|
|
367
|
+
result = explain(engine, node.id, exp.query)
|
|
368
|
+
hits = dict(retrieve(engine, exp.query, persist=False))
|
|
369
|
+
rank = next((i for i, nid in enumerate(hits, start=1) if nid == node.id), None)
|
|
370
|
+
expected_final = (result["reranker_score"] if result["final_score_kind"] == "reranker"
|
|
371
|
+
else hits.get(node.id)) if rank is not None else None
|
|
372
|
+
if (result["retrieved"] != exp.retrieved or result["rank"] != rank
|
|
373
|
+
or result["retrieval_score"] != hits.get(node.id)
|
|
374
|
+
or result["final_score"] != expected_final):
|
|
375
|
+
failures.append(f"explanation disagrees with retrieval: {exp.node_text!r}")
|
|
376
|
+
contributions = sum(result[channel]["rrf_contribution"] for channel in ("bm25", "dense"))
|
|
377
|
+
if not math.isclose(result["rrf"]["score"], contributions):
|
|
378
|
+
failures.append(f"explanation RRF contributions do not sum: {exp.node_text!r}")
|
|
379
|
+
connected = {edge[side] for edge in result["graph_context"]
|
|
380
|
+
for side in ("source", "target")} - {node.id}
|
|
381
|
+
for text in exp.related_texts:
|
|
382
|
+
related = find_node_by_text(engine.brain, text)
|
|
383
|
+
if related is None or related.id not in connected:
|
|
384
|
+
failures.append(f"explanation missing connection: {text!r}")
|
|
385
|
+
return failures
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def verify_neighborhood(brain: Brain, expectations: list) -> list[str]:
|
|
389
|
+
"""Check neighborhood reachability against NeighborhoodExpectation entries.
|
|
390
|
+
|
|
391
|
+
Degrades gracefully when graph_engine.graph does not exist: the gate test
|
|
392
|
+
must fail for the RIGHT reason (expectation violation), never crash the
|
|
393
|
+
harness with an ImportError (same trap as a missing no-op kwarg stub).
|
|
394
|
+
"""
|
|
395
|
+
try:
|
|
396
|
+
from .graph import neighbors
|
|
397
|
+
except ImportError:
|
|
398
|
+
return ["graph traversal not implemented (graph_engine/graph.py missing)"]
|
|
399
|
+
failures: list[str] = []
|
|
400
|
+
if not expectations:
|
|
401
|
+
return failures
|
|
402
|
+
for exp in expectations:
|
|
403
|
+
node = find_node_by_text(brain, exp.node_text)
|
|
404
|
+
if node is None:
|
|
405
|
+
continue # missing nodes already reported by verify_end_state
|
|
406
|
+
# expected + absent in ONE call set: include_pending=True because the
|
|
407
|
+
# harness verifies what the graph CONTAINS, not the review gate state.
|
|
408
|
+
found = neighbors(brain, node.id, hops=exp.hops, include_pending=True,
|
|
409
|
+
limit=None)
|
|
410
|
+
found_ids = {n.id for n in found}
|
|
411
|
+
for want in exp.expected_neighbor_texts:
|
|
412
|
+
w = find_node_by_text(brain, want)
|
|
413
|
+
if w is None or w.id not in found_ids:
|
|
414
|
+
failures.append(
|
|
415
|
+
f"neighborhood '{exp.node_text[:40]}': {want!r} not within "
|
|
416
|
+
f"{exp.hops} hops")
|
|
417
|
+
for avoid in exp.absent_neighbor_texts:
|
|
418
|
+
w = find_node_by_text(brain, avoid)
|
|
419
|
+
if w is not None and w.id in found_ids:
|
|
420
|
+
failures.append(
|
|
421
|
+
f"neighborhood '{exp.node_text[:40]}': {avoid!r} must NOT be "
|
|
422
|
+
f"within {exp.hops} hops")
|
|
423
|
+
return failures
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def verify_communities(brain: Brain, expectations: list) -> list[str]:
|
|
427
|
+
"""Check the final partition against CommunityExpectation entries.
|
|
428
|
+
|
|
429
|
+
All three assertion families run together on purpose: `together` alone
|
|
430
|
+
is trivially green if everything lands in one community, `apart` alone
|
|
431
|
+
if every node is its own community, and `gap` alone if the analyzer
|
|
432
|
+
reported every pair. Requires all three to hold.
|
|
433
|
+
"""
|
|
434
|
+
from .communities import analyze_communities
|
|
435
|
+
|
|
436
|
+
failures: list[str] = []
|
|
437
|
+
if not expectations:
|
|
438
|
+
return failures
|
|
439
|
+
# One analysis per expectation list, min_size = the strictest requested.
|
|
440
|
+
min_size = min(exp.min_size for exp in expectations)
|
|
441
|
+
# top is generous on purpose: the verifier needs the COMPLETE gap list
|
|
442
|
+
# (truncating to the CLI default could hide a pair the oracle asserts).
|
|
443
|
+
rep = analyze_communities(brain, min_size=min_size, top=10_000,
|
|
444
|
+
include_pending=True, with_members=True)
|
|
445
|
+
# Community membership lookup: id -> canonical community index.
|
|
446
|
+
id2com: dict[str, int] = {}
|
|
447
|
+
for c in rep.communities:
|
|
448
|
+
for nid in c.members:
|
|
449
|
+
id2com[nid] = c.id
|
|
450
|
+
gap_pairs: set[tuple[int, int]] = {(g.a, g.b) for g in rep.gaps}
|
|
451
|
+
|
|
452
|
+
def _resolve(text: str):
|
|
453
|
+
node = find_node_by_text(brain, text)
|
|
454
|
+
return node.id if node else None
|
|
455
|
+
|
|
456
|
+
for exp in expectations:
|
|
457
|
+
for group in exp.together:
|
|
458
|
+
ids = [_resolve(t) for t in group]
|
|
459
|
+
if any(i is None for i in ids):
|
|
460
|
+
continue # missing nodes already reported by verify_end_state
|
|
461
|
+
coms = {id2com[i] for i in ids}
|
|
462
|
+
if len(coms) != 1:
|
|
463
|
+
failures.append(
|
|
464
|
+
f"together violated: {group!r} spans communities {sorted(coms)}")
|
|
465
|
+
for a, b in exp.apart:
|
|
466
|
+
ia, ib = _resolve(a), _resolve(b)
|
|
467
|
+
if ia is None or ib is None:
|
|
468
|
+
continue
|
|
469
|
+
if id2com.get(ia) == id2com.get(ib):
|
|
470
|
+
failures.append(
|
|
471
|
+
f"apart violated: {a!r} and {b!r} share community {id2com.get(ia)}")
|
|
472
|
+
for a, b in exp.gap:
|
|
473
|
+
ia, ib = _resolve(a), _resolve(b)
|
|
474
|
+
if ia is None or ib is None:
|
|
475
|
+
continue
|
|
476
|
+
pair = (min(id2com.get(ia, -1), id2com.get(ib, -1)),
|
|
477
|
+
max(id2com.get(ia, -1), id2com.get(ib, -1)))
|
|
478
|
+
if pair not in gap_pairs:
|
|
479
|
+
failures.append(
|
|
480
|
+
f"gap not reported: {a!r} <-> {b!r} (communities {pair[0]}/{pair[1]})")
|
|
481
|
+
return failures
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
# Runner (pass^k)
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
EngineFactory = Callable[[], BrainEngine]
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def run_eval(task: EvalTask, engine_factory: EngineFactory, k: int = 1) -> EvalResult:
|
|
492
|
+
"""Run the task k times against a fresh brain; all runs must be green."""
|
|
493
|
+
for run in range(1, k + 1):
|
|
494
|
+
engine = engine_factory()
|
|
495
|
+
if task.reranker is not None:
|
|
496
|
+
engine.reranker = task.reranker
|
|
497
|
+
for text, kwargs in task.ingests:
|
|
498
|
+
engine.ingest(text, **kwargs)
|
|
499
|
+
for action in task.actions:
|
|
500
|
+
action(engine)
|
|
501
|
+
failures = verify_end_state(engine.brain, task.oracle)
|
|
502
|
+
failures += verify_retrieval(engine, task.oracle.retrieval)
|
|
503
|
+
failures += verify_explanations(engine, task.oracle.explanations)
|
|
504
|
+
failures += verify_communities(engine.brain, task.oracle.communities)
|
|
505
|
+
failures += verify_neighborhood(engine.brain, task.oracle.neighborhoods)
|
|
506
|
+
if failures:
|
|
507
|
+
return EvalResult(task.id, task.name, False, failures, run)
|
|
508
|
+
return EvalResult(task.id, task.name, True, [], k)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def run_tasks(tasks: list[EvalTask], engine_factory: EngineFactory, k: int = 1) -> list[EvalResult]:
|
|
512
|
+
return [run_eval(t, engine_factory, k) for t in tasks]
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def report(results: list[EvalResult]) -> tuple[int, list[EvalResult]]:
|
|
516
|
+
"""Returns (number green, all) — useful for CLI/logging."""
|
|
517
|
+
failed = [r for r in results if not r.passed]
|
|
518
|
+
return len(results) - len(failed), failed
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ---------------------------------------------------------------------------
|
|
522
|
+
# Golden set — regression on every engine change (MUST be green)
|
|
523
|
+
# ---------------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
def _seed_episodic_consolidation(engine: BrainEngine) -> None:
|
|
526
|
+
for _ in range(2):
|
|
527
|
+
engine.observe("User requested brief replies", allow_duplicates=True,
|
|
528
|
+
observed_at="2000-01-01T00:00:00Z")
|
|
529
|
+
engine.observe("A button was clicked", observed_at="2000-01-01T00:00:00Z")
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _consolidate_episodic(engine: BrainEngine) -> None:
|
|
533
|
+
from .consolidation import ConsolidationConfig, consolidate
|
|
534
|
+
consolidate(engine.brain, config=ConsolidationConfig(),
|
|
535
|
+
extractor=lambda evidence: "User prefers concise answers")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _dream_lifecycle() -> Callable[[BrainEngine], None]:
|
|
539
|
+
"""Test action: the lifecycle pass under the case's gates."""
|
|
540
|
+
def action(engine: BrainEngine) -> None:
|
|
541
|
+
from .dream import lifecycle
|
|
542
|
+
lifecycle(engine.brain, min_recall=1, min_degree=2, stale_days=0,
|
|
543
|
+
max_degree=2)
|
|
544
|
+
return action
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _record_recall(query: str, texts: list[str]) -> Callable[[BrainEngine], None]:
|
|
548
|
+
"""Test action: record a recall of the given nodes (the promotion signal)."""
|
|
549
|
+
def action(engine: BrainEngine) -> None:
|
|
550
|
+
from .recall import record
|
|
551
|
+
ids = [n.id for n in engine.brain.read_nodes()
|
|
552
|
+
if n.text.strip() in {t.strip() for t in texts}]
|
|
553
|
+
record(engine.brain, query, ids)
|
|
554
|
+
return action
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _agent_remember(text: str, relation_target: str | None = None) -> Callable[[BrainEngine], None]:
|
|
558
|
+
"""Test action: an agent writes a note (Welle C write path)."""
|
|
559
|
+
def action(engine: BrainEngine) -> None:
|
|
560
|
+
from .agent_memory import remember
|
|
561
|
+
relations = [(relation_target, "extends")] if relation_target else None
|
|
562
|
+
remember(engine, text, relations=relations)
|
|
563
|
+
return action
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _agent_forget(text: str, reason: str) -> Callable[[BrainEngine], None]:
|
|
567
|
+
"""Test action: an agent forgets a node — tombstoned, never deleted."""
|
|
568
|
+
def action(engine: BrainEngine) -> None:
|
|
569
|
+
from .agent_memory import forget
|
|
570
|
+
node = find_node_by_text(engine.brain, text)
|
|
571
|
+
if node is not None:
|
|
572
|
+
forget(engine.brain, node.id, reason=reason)
|
|
573
|
+
return action
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _link_same_as(source_text: str, target_text: str) -> Callable[[BrainEngine], None]:
|
|
577
|
+
def action(engine: BrainEngine) -> None:
|
|
578
|
+
s = find_node_by_text(engine.brain, source_text)
|
|
579
|
+
t = find_node_by_text(engine.brain, target_text)
|
|
580
|
+
if s and t:
|
|
581
|
+
engine.link(s.id, t.id, "same_as")
|
|
582
|
+
return action
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _force_kind(source_text: str, target_text: str, kind: str) -> Callable[[BrainEngine], None]:
|
|
586
|
+
"""Test action: rewrite the kind of an existing similarity edge.
|
|
587
|
+
|
|
588
|
+
Simulates drift/legacy data (an edge whose kind no longer matches the band
|
|
589
|
+
rule its cosine implies) so `dream.refresh()` has something to re-derive.
|
|
590
|
+
"""
|
|
591
|
+
def action(engine: BrainEngine) -> None:
|
|
592
|
+
s = find_node_by_text(engine.brain, source_text)
|
|
593
|
+
t = find_node_by_text(engine.brain, target_text)
|
|
594
|
+
if not s or not t:
|
|
595
|
+
return
|
|
596
|
+
edges = engine.brain.read_edges(include_rejected=True)
|
|
597
|
+
for edge in edges:
|
|
598
|
+
if ({edge.source, edge.target} == {s.id, t.id}
|
|
599
|
+
and edge.kind in ("extends", "similar")):
|
|
600
|
+
edge.kind = kind
|
|
601
|
+
engine.brain.write_edges(edges)
|
|
602
|
+
return action
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _dream_distill(engine: BrainEngine) -> None:
|
|
606
|
+
from .dream import distill
|
|
607
|
+
distill(engine.brain, min_size=2, limit=5, members_per_summary=3)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _dream_refresh(engine: BrainEngine) -> None:
|
|
611
|
+
from .dream import refresh
|
|
612
|
+
refresh(engine.brain)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
GOLDEN_SET: list[EvalTask] = [
|
|
616
|
+
EvalTask(
|
|
617
|
+
id="dup-exact",
|
|
618
|
+
name="exact duplicate gets merged (sources accumulate)",
|
|
619
|
+
ingests=[
|
|
620
|
+
("Katzen jagen Maeuse nachts", {"source": "agent/test"}),
|
|
621
|
+
("Katzen jagen Maeuse nachts", {"source": "human"}),
|
|
622
|
+
],
|
|
623
|
+
oracle=EvalOracle(
|
|
624
|
+
node_count=1,
|
|
625
|
+
duplicate_merged=[("Katzen jagen Maeuse nachts", ["agent/test", "human"])],
|
|
626
|
+
),
|
|
627
|
+
),
|
|
628
|
+
EvalTask(
|
|
629
|
+
id="dup-case-whitespace",
|
|
630
|
+
name="duplicate merged regardless of case + whitespace",
|
|
631
|
+
ingests=[
|
|
632
|
+
("Katzen jagen Maeuse nachts", {"source": "a"}),
|
|
633
|
+
(" katzen JAGEN maeuse NACHTS ", {"source": "b"}),
|
|
634
|
+
],
|
|
635
|
+
oracle=EvalOracle(node_count=1, duplicate_merged=[("katzen jagen maeuse nachts", ["a", "b"])]),
|
|
636
|
+
),
|
|
637
|
+
EvalTask(
|
|
638
|
+
id="retrieval-explain",
|
|
639
|
+
name="Retrieval explanations reconstruct scores and show accepted graph context",
|
|
640
|
+
ingests=[("alpha beta", {}), ("alpha gamma", {}), ("omega psi", {})],
|
|
641
|
+
actions=[_link_same_as("alpha beta", "alpha gamma")],
|
|
642
|
+
oracle=EvalOracle(explanations=[
|
|
643
|
+
ExplanationExpectation(query="alpha", node_text="alpha beta",
|
|
644
|
+
related_texts=["alpha gamma"]),
|
|
645
|
+
ExplanationExpectation(query="alpha", node_text="omega psi", retrieved=False),
|
|
646
|
+
]),
|
|
647
|
+
),
|
|
648
|
+
EvalTask(
|
|
649
|
+
id="dup-disabled",
|
|
650
|
+
name="allow_duplicates=True creates a second node",
|
|
651
|
+
ingests=[
|
|
652
|
+
("Katzen jagen Maeuse nachts", {}),
|
|
653
|
+
("Katzen jagen Maeuse nachts", {"allow_duplicates": True}),
|
|
654
|
+
],
|
|
655
|
+
oracle=EvalOracle(node_count=2),
|
|
656
|
+
),
|
|
657
|
+
EvalTask(
|
|
658
|
+
id="no-false-positive",
|
|
659
|
+
name="unrelated texts are not deduplicated",
|
|
660
|
+
ingests=[
|
|
661
|
+
("Katzen jagen Maeuse nachts", {}),
|
|
662
|
+
("Rust Compiler borrow checker lifetime Regeln", {}),
|
|
663
|
+
],
|
|
664
|
+
oracle=EvalOracle(node_count=2, nodes_present=[
|
|
665
|
+
"Katzen jagen Maeuse nachts",
|
|
666
|
+
"Rust Compiler borrow checker lifetime Regeln",
|
|
667
|
+
]),
|
|
668
|
+
),
|
|
669
|
+
EvalTask(
|
|
670
|
+
id="edge-similar",
|
|
671
|
+
name="similar nodes get connected by an edge (pending <0.95)",
|
|
672
|
+
ingests=[
|
|
673
|
+
("katze hund tier futter", {}),
|
|
674
|
+
("katze hund tier spiel", {}),
|
|
675
|
+
],
|
|
676
|
+
oracle=EvalOracle(
|
|
677
|
+
node_count=2,
|
|
678
|
+
edges=[EdgeExpectation("katze hund tier futter", "katze hund tier spiel", "*", pending=True)],
|
|
679
|
+
),
|
|
680
|
+
),
|
|
681
|
+
EvalTask(
|
|
682
|
+
id="conf-auto-accept",
|
|
683
|
+
name="V2: Confidence >=0.95 → edge auto-accepted (pending=False)",
|
|
684
|
+
ingests=[
|
|
685
|
+
("x x x x x y y y y y z z z z z", {}),
|
|
686
|
+
("x x x x x y y y y y z z z z w", {"allow_duplicates": True}),
|
|
687
|
+
],
|
|
688
|
+
oracle=EvalOracle(
|
|
689
|
+
node_count=2,
|
|
690
|
+
edges=[EdgeExpectation(
|
|
691
|
+
"x x x x x y y y y y z z z z z",
|
|
692
|
+
"x x x x x y y y y y z z z z w",
|
|
693
|
+
"*", pending=False, min_confidence=0.95,
|
|
694
|
+
)],
|
|
695
|
+
),
|
|
696
|
+
),
|
|
697
|
+
EvalTask(
|
|
698
|
+
id="edge-no-false-link",
|
|
699
|
+
name="unrelated nodes are NOT connected",
|
|
700
|
+
ingests=[
|
|
701
|
+
("katze hund tier futter", {}),
|
|
702
|
+
("quantenmechanik wellenfunktion schroedinger", {}),
|
|
703
|
+
],
|
|
704
|
+
oracle=EvalOracle(
|
|
705
|
+
node_count=2,
|
|
706
|
+
no_edge=[EdgeExpectation("katze hund tier futter", "quantenmechanik wellenfunktion schroedinger", "*")],
|
|
707
|
+
),
|
|
708
|
+
),
|
|
709
|
+
EvalTask(
|
|
710
|
+
id="retrieval-hybrid",
|
|
711
|
+
name="V2: hybrid retrieval finds the matching node, not unrelated ones",
|
|
712
|
+
ingests=[
|
|
713
|
+
("katze hund tier futter", {}),
|
|
714
|
+
("quantenmechanik wellenfunktion schroedinger", {}),
|
|
715
|
+
],
|
|
716
|
+
oracle=EvalOracle(
|
|
717
|
+
node_count=2,
|
|
718
|
+
retrieval=[RetrievalExpectation(
|
|
719
|
+
query="katze futter",
|
|
720
|
+
top=1,
|
|
721
|
+
includes=["katze hund tier futter"],
|
|
722
|
+
excludes=["quantenmechanik wellenfunktion schroedinger"],
|
|
723
|
+
)],
|
|
724
|
+
),
|
|
725
|
+
),
|
|
726
|
+
EvalTask(
|
|
727
|
+
id="retrieval-rerank-honored",
|
|
728
|
+
name="V2: rerank pass decides the final ranking (cross-encoder pipeline)",
|
|
729
|
+
ingests=[
|
|
730
|
+
("aaa bbb ccc", {}),
|
|
731
|
+
("ddd eee fff", {}),
|
|
732
|
+
],
|
|
733
|
+
oracle=EvalOracle(
|
|
734
|
+
node_count=2,
|
|
735
|
+
retrieval=[RetrievalExpectation(
|
|
736
|
+
query="ddd eee fff",
|
|
737
|
+
top=1,
|
|
738
|
+
includes=["aaa bbb ccc"],
|
|
739
|
+
excludes=["ddd eee fff"],
|
|
740
|
+
)],
|
|
741
|
+
),
|
|
742
|
+
# ReverseReranker is the deterministic stand-in for a cross-encoder:
|
|
743
|
+
# hybrid would rank "ddd eee fff" first; the rerank pass reverses that.
|
|
744
|
+
# Green only if retrieve() actually honors the reranker.
|
|
745
|
+
reranker=ReverseReranker(),
|
|
746
|
+
),
|
|
747
|
+
EvalTask(
|
|
748
|
+
id="taxonomy-procedural",
|
|
749
|
+
name="procedural node carries type=procedural",
|
|
750
|
+
ingests=[
|
|
751
|
+
("Wie ingestiere ich Research: ig ingest ...", {"ntype": "procedural"}),
|
|
752
|
+
],
|
|
753
|
+
oracle=EvalOracle(node_count=1, nodes_present=["Wie ingestiere ich Research: ig ingest ..."]),
|
|
754
|
+
),
|
|
755
|
+
EvalTask(
|
|
756
|
+
id="same-as-multilingual",
|
|
757
|
+
name="multilingual pair connected via manual same_as link",
|
|
758
|
+
ingests=[
|
|
759
|
+
("Katzen jagen Maeuse", {}),
|
|
760
|
+
("Cats hunt mice", {"allow_duplicates": True}),
|
|
761
|
+
],
|
|
762
|
+
actions=[_link_same_as("Katzen jagen Maeuse", "Cats hunt mice")],
|
|
763
|
+
oracle=EvalOracle(
|
|
764
|
+
node_count=2,
|
|
765
|
+
edges=[EdgeExpectation("Katzen jagen Maeuse", "Cats hunt mice", "same_as")],
|
|
766
|
+
),
|
|
767
|
+
),
|
|
768
|
+
EvalTask(
|
|
769
|
+
id="hygiene-promote",
|
|
770
|
+
name="V2: dual buffer — consolidation promotes probation nodes to active",
|
|
771
|
+
ingests=[
|
|
772
|
+
("katze hund tier futter", {}),
|
|
773
|
+
("quantenmechanik wellenfunktion schroedinger", {}),
|
|
774
|
+
],
|
|
775
|
+
actions=[lambda e: e.consolidate()],
|
|
776
|
+
oracle=EvalOracle(
|
|
777
|
+
node_count=2,
|
|
778
|
+
node_status={
|
|
779
|
+
"katze hund tier futter": "active",
|
|
780
|
+
"quantenmechanik wellenfunktion schroedinger": "active",
|
|
781
|
+
},
|
|
782
|
+
),
|
|
783
|
+
),
|
|
784
|
+
EvalTask(
|
|
785
|
+
id="hygiene-demote",
|
|
786
|
+
name="V2: graceful degradation — unused node gets tombstoned",
|
|
787
|
+
ingests=[
|
|
788
|
+
("katze hund tier futter", {}),
|
|
789
|
+
("quantenmechanik wellenfunktion schroedinger", {}),
|
|
790
|
+
],
|
|
791
|
+
actions=[
|
|
792
|
+
lambda e: e.consolidate(),
|
|
793
|
+
lambda e: e.demote_forgotten(
|
|
794
|
+
lambda n: "tombstone" if "katze" in n.text else "record"
|
|
795
|
+
),
|
|
796
|
+
],
|
|
797
|
+
oracle=EvalOracle(
|
|
798
|
+
node_count=2,
|
|
799
|
+
node_status={
|
|
800
|
+
"katze hund tier futter": "tombstone",
|
|
801
|
+
"quantenmechanik wellenfunktion schroedinger": "active",
|
|
802
|
+
},
|
|
803
|
+
),
|
|
804
|
+
),
|
|
805
|
+
EvalTask(
|
|
806
|
+
id="intent-contradiction",
|
|
807
|
+
name="V2: contradictory statement -> 'contradicts' edge auto-detected",
|
|
808
|
+
ingests=[
|
|
809
|
+
("Die Erde ist eine Scheibe", {}),
|
|
810
|
+
("Die Erde ist keine Scheibe", {}),
|
|
811
|
+
],
|
|
812
|
+
oracle=EvalOracle(
|
|
813
|
+
node_count=2,
|
|
814
|
+
edges=[EdgeExpectation("Die Erde ist eine Scheibe", "Die Erde ist keine Scheibe", "contradicts")],
|
|
815
|
+
),
|
|
816
|
+
),
|
|
817
|
+
EvalTask(
|
|
818
|
+
id="intent-supersedes",
|
|
819
|
+
name="V2: newer statement supersedes older → 'supersedes' edge",
|
|
820
|
+
ingests=[
|
|
821
|
+
("API v1 wird verwendet", {}),
|
|
822
|
+
("API v2 ersetzt v1", {}),
|
|
823
|
+
],
|
|
824
|
+
oracle=EvalOracle(
|
|
825
|
+
node_count=2,
|
|
826
|
+
edges=[EdgeExpectation("API v1 wird verwendet", "API v2 ersetzt v1", "supersedes")],
|
|
827
|
+
),
|
|
828
|
+
),
|
|
829
|
+
EvalTask(
|
|
830
|
+
id="admit-rule-no-relations",
|
|
831
|
+
name="V2: admit rule — node without relations stays in probation (admit_required)",
|
|
832
|
+
ingests=[
|
|
833
|
+
("xyzvw abcdefgh ijklmnop", {}),
|
|
834
|
+
],
|
|
835
|
+
actions=[lambda e: e.consolidate(admit_required=True)],
|
|
836
|
+
oracle=EvalOracle(
|
|
837
|
+
node_count=1,
|
|
838
|
+
node_status={"xyzvw abcdefgh ijklmnop": "probation"},
|
|
839
|
+
),
|
|
840
|
+
),
|
|
841
|
+
EvalTask(
|
|
842
|
+
id="admit-rule-with-relations",
|
|
843
|
+
name="V2: admit rule — node WITH declared relations becomes active",
|
|
844
|
+
ingests=[
|
|
845
|
+
("aaa bbb ccc", {}),
|
|
846
|
+
("aaa bbb ccc ddd", {"allow_duplicates": True,
|
|
847
|
+
"relations": [("aaa bbb ccc", "extends")]}),
|
|
848
|
+
],
|
|
849
|
+
actions=[lambda e: e.consolidate(admit_required=True)],
|
|
850
|
+
oracle=EvalOracle(
|
|
851
|
+
node_count=2,
|
|
852
|
+
node_status={
|
|
853
|
+
"aaa bbb ccc": "active",
|
|
854
|
+
"aaa bbb ccc ddd": "active",
|
|
855
|
+
},
|
|
856
|
+
),
|
|
857
|
+
),
|
|
858
|
+
# GOLDEN (flip 2026-09-10, was `roadmap-confidence-floor`) — Tier-3 self-extension:
|
|
859
|
+
# configurable confidence floor for auto edge suggestions (per-call env kwarg,
|
|
860
|
+
# IG_EDGE_CONF_FLOOR, default 0.0 = no behavior change). Suggestions below the
|
|
861
|
+
# floor are dropped instead of landing pending — protects autonomous cycles
|
|
862
|
+
# from a flood of low-confidence edges. (HashEmbedder measurement: the two
|
|
863
|
+
# texts sit at cos≈0.653 → pending band 0.45–0.95, below floor 0.95.)
|
|
864
|
+
EvalTask(
|
|
865
|
+
id="roadmap-confidence-floor",
|
|
866
|
+
name="Confidence floor rejects weak auto edge suggestions",
|
|
867
|
+
ingests=[
|
|
868
|
+
("Agentenplanung zerlegt langfristige Aufgaben in hierarchische "
|
|
869
|
+
"Teilziele und prueft Zwischenergebnisse gegen den Zielzustand.", {}),
|
|
870
|
+
("Agentenplanung in Multi-Agent-Systemen verteilt hierarchische "
|
|
871
|
+
"Teilziele und prueft Zwischenergebnisse gegenseitig.",
|
|
872
|
+
{"env": {"IG_EDGE_CONF_FLOOR": "0.95"}}),
|
|
873
|
+
],
|
|
874
|
+
oracle=EvalOracle(
|
|
875
|
+
node_count=2,
|
|
876
|
+
# With floor: the 0.653 suggestion is rejected (no similar edge).
|
|
877
|
+
edges=[],
|
|
878
|
+
no_edge=[EdgeExpectation(
|
|
879
|
+
source="Agentenplanung in Multi-Agent-Systemen verteilt",
|
|
880
|
+
target="Agentenplanung zerlegt langfristige Aufgaben",
|
|
881
|
+
kind="similar")],
|
|
882
|
+
),
|
|
883
|
+
),
|
|
884
|
+
|
|
885
|
+
# Report #3 (2026-09-15): topology communities — two topical clusters form
|
|
886
|
+
# two communities with a structural gap. Registered RED, implemented, then
|
|
887
|
+
# flipped (pass^3 + full suite green).
|
|
888
|
+
EvalTask(
|
|
889
|
+
id="roadmap-communities-two-clusters",
|
|
890
|
+
name="Topology: two topical clusters form two communities with a structural gap",
|
|
891
|
+
ingests=[
|
|
892
|
+
("alpha beta gamma delta", {}),
|
|
893
|
+
("alpha beta gamma epsilon", {}),
|
|
894
|
+
("alpha beta gamma zeta", {}),
|
|
895
|
+
("omega psi chi phi", {}),
|
|
896
|
+
("omega psi chi kappa", {}),
|
|
897
|
+
("omega psi chi lambda", {}),
|
|
898
|
+
],
|
|
899
|
+
oracle=EvalOracle(
|
|
900
|
+
node_count=6,
|
|
901
|
+
communities=[CommunityExpectation(
|
|
902
|
+
together=[
|
|
903
|
+
["alpha beta gamma delta", "alpha beta gamma epsilon",
|
|
904
|
+
"alpha beta gamma zeta"],
|
|
905
|
+
["omega psi chi phi", "omega psi chi kappa",
|
|
906
|
+
"omega psi chi lambda"],
|
|
907
|
+
],
|
|
908
|
+
apart=[("alpha beta gamma delta", "omega psi chi phi")],
|
|
909
|
+
gap=[("alpha beta gamma delta", "omega psi chi phi")],
|
|
910
|
+
min_size=2,
|
|
911
|
+
)],
|
|
912
|
+
),
|
|
913
|
+
),
|
|
914
|
+
# Report #7 (2026-09-15): BRAIN_REPORT digest renders non-empty, sectioned,
|
|
915
|
+
# content-asserted output. Registered RED, implemented, then flipped.
|
|
916
|
+
EvalTask(
|
|
917
|
+
id="roadmap-brain-report",
|
|
918
|
+
name="BRAIN_REPORT renders non-empty, sectioned, content-asserted output",
|
|
919
|
+
ingests=[
|
|
920
|
+
("Die Erde ist eine Scheibe", {}),
|
|
921
|
+
("Die Erde ist keine Scheibe", {}),
|
|
922
|
+
],
|
|
923
|
+
oracle=EvalOracle(
|
|
924
|
+
node_count=2,
|
|
925
|
+
report_contains=["BRAIN_REPORT", "Intent review queue",
|
|
926
|
+
"Die Erde ist eine Scheibe"],
|
|
927
|
+
),
|
|
928
|
+
),
|
|
929
|
+
# Report #1 (2026-09-15): graph traversal over live edges (flipped). MEASURED under
|
|
930
|
+
# HashEmbedder: the three agent-memory texts sit at cos 0.875/0.875/0.75
|
|
931
|
+
# (similar/similar/extends — inside the 0.75/0.45 bands), the gardening
|
|
932
|
+
# text at 0.13-0.27 (no edges). The MCP surface itself is a read-only
|
|
933
|
+
# projection and CANNOT be an EvalTask (the harness verifies brain
|
|
934
|
+
# end-state only) — same judgement as Late Chunking above; traversal is
|
|
935
|
+
# the measurable new capability underneath it.
|
|
936
|
+
EvalTask(
|
|
937
|
+
id="roadmap-neighbors",
|
|
938
|
+
name="graph traversal reaches hub neighbors at hop 1, isolates stay out",
|
|
939
|
+
ingests=[
|
|
940
|
+
("agent memory systems store knowledge graphs for retrieval", {}),
|
|
941
|
+
("agent memory systems store knowledge graphs for retrieval and search", {}),
|
|
942
|
+
("agent memory systems store knowledge graphs for retrieval but slower", {}),
|
|
943
|
+
("unrelated note about gardening tomatoes and weather", {}),
|
|
944
|
+
],
|
|
945
|
+
oracle=EvalOracle(
|
|
946
|
+
node_count=4,
|
|
947
|
+
neighborhoods=[
|
|
948
|
+
NeighborhoodExpectation(
|
|
949
|
+
node_text="agent memory systems store knowledge graphs for retrieval",
|
|
950
|
+
expected_neighbor_texts=[
|
|
951
|
+
"agent memory systems store knowledge graphs for retrieval and search",
|
|
952
|
+
"agent memory systems store knowledge graphs for retrieval but slower",
|
|
953
|
+
],
|
|
954
|
+
absent_neighbor_texts=[
|
|
955
|
+
"unrelated note about gardening tomatoes and weather",
|
|
956
|
+
],
|
|
957
|
+
hops=1,
|
|
958
|
+
),
|
|
959
|
+
],
|
|
960
|
+
),
|
|
961
|
+
),
|
|
962
|
+
# Intent fan-out dam (2026-09-18): registered RED, implemented, flipped.
|
|
963
|
+
# Intent edges carry confidence=None, so the confidence bands can never
|
|
964
|
+
# judge them; the marker heuristic is their only producer and mass-fires in
|
|
965
|
+
# a homogeneous brain — live measurement before the dam: 168 intent edges
|
|
966
|
+
# ever created, 66 invalidated again (39 % false), one source holding 10
|
|
967
|
+
# auto-accepted `contradicts`, 26 false edges added in one cycle.
|
|
968
|
+
# MEASURED fixture (HashEmbedder, real engine path): the marker text fires 6
|
|
969
|
+
# `supersedes` edges at cos 0.866/0.722 (threshold 0.45) and the maximum
|
|
970
|
+
# pairwise cosine is 0.866 < dedupe 0.92, so the case fails on the CAP, not
|
|
971
|
+
# on a missing edge.
|
|
972
|
+
EvalTask(
|
|
973
|
+
id="roadmap-intent-fanout-cap",
|
|
974
|
+
name="Intent edges: auto-accepted fan-out per source is capped, the rest stay pending",
|
|
975
|
+
ingests=[
|
|
976
|
+
("alpha beta gamma delta training pipeline", {}),
|
|
977
|
+
("alpha beta gamma epsilon training pipeline", {}),
|
|
978
|
+
("alpha beta gamma zeta training pipeline", {}),
|
|
979
|
+
("alpha beta gamma eta training pipeline", {}),
|
|
980
|
+
("alpha beta gamma theta training pipeline", {}),
|
|
981
|
+
("alpha beta gamma iota training pipeline", {}),
|
|
982
|
+
("alpha beta gamma kappa training pipeline ersetzt delta", {}),
|
|
983
|
+
],
|
|
984
|
+
oracle=EvalOracle(
|
|
985
|
+
node_count=7,
|
|
986
|
+
max_auto_intent_per_source=2,
|
|
987
|
+
),
|
|
988
|
+
),
|
|
989
|
+
# Welle A (2026-09-18): the two signals the consolidation half of a memory
|
|
990
|
+
# system runs on — registered RED, implemented, flipped in one session.
|
|
991
|
+
# `roadmap-edge-origin`: a maintenance pass may rewrite heuristic edges but
|
|
992
|
+
# never user-authored ones, so the provenance has to be stored, not inferred
|
|
993
|
+
# from the text (the 96-edge backlog cleanup was exactly that inference by
|
|
994
|
+
# hand). `roadmap-recall-tracking`: promotion/decay needs actual use —
|
|
995
|
+
# recall_count + distinct query fingerprints, folded from a local ledger so
|
|
996
|
+
# the read path stays cheap and git-clean.
|
|
997
|
+
EvalTask(
|
|
998
|
+
id="roadmap-edge-origin",
|
|
999
|
+
name="Edges carry their provenance: suggester kNN vs manual link",
|
|
1000
|
+
ingests=[
|
|
1001
|
+
("agent memory systems store knowledge graphs for retrieval", {}),
|
|
1002
|
+
("agent memory systems store knowledge graphs for retrieval and search", {}),
|
|
1003
|
+
("Retrieval-Augmented Generation connects a model to external documents", {}),
|
|
1004
|
+
("Graph RAG combines vector search with multi-hop traversal", {}),
|
|
1005
|
+
],
|
|
1006
|
+
actions=[_link_same_as(
|
|
1007
|
+
"Retrieval-Augmented Generation connects a model to external documents",
|
|
1008
|
+
"Graph RAG combines vector search with multi-hop traversal")],
|
|
1009
|
+
oracle=EvalOracle(
|
|
1010
|
+
node_count=4,
|
|
1011
|
+
edges=[
|
|
1012
|
+
# MEASURED (neighbors fixture): these two sit at cos 0.875, i.e.
|
|
1013
|
+
# the `similar` band — the suggester produces the edge.
|
|
1014
|
+
EdgeExpectation(
|
|
1015
|
+
source="agent memory systems store knowledge graphs for retrieval and search",
|
|
1016
|
+
target="agent memory systems store knowledge graphs for retrieval",
|
|
1017
|
+
kind="similar", origin="suggester"),
|
|
1018
|
+
EdgeExpectation(
|
|
1019
|
+
source="Retrieval-Augmented Generation connects a model to external documents",
|
|
1020
|
+
target="Graph RAG combines vector search with multi-hop traversal",
|
|
1021
|
+
kind="same_as", origin="manual"),
|
|
1022
|
+
],
|
|
1023
|
+
),
|
|
1024
|
+
),
|
|
1025
|
+
EvalTask(
|
|
1026
|
+
id="roadmap-recall-tracking",
|
|
1027
|
+
name="Recall tracking: retrieved nodes carry recall counts, untouched ones stay at 0",
|
|
1028
|
+
ingests=[
|
|
1029
|
+
("alpha beta gamma delta training pipeline", {}),
|
|
1030
|
+
("omega psi chi phi gardening tomatoes", {}),
|
|
1031
|
+
],
|
|
1032
|
+
actions=[
|
|
1033
|
+
lambda e: retrieve(e, "alpha beta gamma delta training pipeline", k=1, track=True),
|
|
1034
|
+
lambda e: retrieve(e, "alpha beta gamma delta training", k=1, track=True),
|
|
1035
|
+
lambda e: aggregate_recalls(e.brain),
|
|
1036
|
+
],
|
|
1037
|
+
oracle=EvalOracle(
|
|
1038
|
+
node_count=2,
|
|
1039
|
+
node_recalls={
|
|
1040
|
+
"alpha beta gamma delta training pipeline": 2,
|
|
1041
|
+
"omega psi chi phi gardening tomatoes": 0,
|
|
1042
|
+
},
|
|
1043
|
+
),
|
|
1044
|
+
),
|
|
1045
|
+
# Welle B (2026-09-18): the consolidation half — the dream pass. Measured
|
|
1046
|
+
# before building: recall-gated promotion had 0 eligible nodes, a degree gate
|
|
1047
|
+
# >= 3 matched 99 % of nodes, the corpus was 27 days old (so no staleness
|
|
1048
|
+
# gate can fire), and the 97 near-dup pairs in the review band are
|
|
1049
|
+
# demonstrably related-but-distinct — so promotion/decay/auto-merge are NOT
|
|
1050
|
+
# part of the pass yet. What IS measurable today: the deterministic refresh
|
|
1051
|
+
# and the distillation of communities (30 communities >= 10 nodes cover 94 %
|
|
1052
|
+
# of the live brain).
|
|
1053
|
+
EvalTask(
|
|
1054
|
+
id="roadmap-dream-refresh",
|
|
1055
|
+
name="Dream refresh re-derives drifted suggester kinds, leaves manual edges alone",
|
|
1056
|
+
ingests=[
|
|
1057
|
+
("agent memory systems store knowledge graphs for retrieval", {}),
|
|
1058
|
+
("agent memory systems store knowledge graphs for retrieval and search", {}),
|
|
1059
|
+
("Retrieval-Augmented Generation connects a model to external documents", {}),
|
|
1060
|
+
("Graph RAG combines vector search with multi-hop traversal", {}),
|
|
1061
|
+
# 5th ingest declares a relation explicitly -> origin="manual". Its
|
|
1062
|
+
# cosine is in the `similar` band, so a pass that ignored provenance
|
|
1063
|
+
# would "correct" a user-authored edge.
|
|
1064
|
+
("agent memory systems store knowledge graphs for retrieval and ranking",
|
|
1065
|
+
{"relations": [("agent memory systems store knowledge graphs for retrieval",
|
|
1066
|
+
"extends")]}),
|
|
1067
|
+
],
|
|
1068
|
+
actions=[
|
|
1069
|
+
# drift the suggester edge (cos 0.875 => must be `similar` again)
|
|
1070
|
+
_force_kind("agent memory systems store knowledge graphs for retrieval and search",
|
|
1071
|
+
"agent memory systems store knowledge graphs for retrieval",
|
|
1072
|
+
"extends"),
|
|
1073
|
+
_dream_refresh,
|
|
1074
|
+
],
|
|
1075
|
+
oracle=EvalOracle(
|
|
1076
|
+
node_count=5,
|
|
1077
|
+
edges=[
|
|
1078
|
+
EdgeExpectation(
|
|
1079
|
+
source="agent memory systems store knowledge graphs for retrieval and search",
|
|
1080
|
+
target="agent memory systems store knowledge graphs for retrieval",
|
|
1081
|
+
kind="similar", origin="suggester"),
|
|
1082
|
+
EdgeExpectation(
|
|
1083
|
+
source="agent memory systems store knowledge graphs for retrieval and ranking",
|
|
1084
|
+
target="agent memory systems store knowledge graphs for retrieval",
|
|
1085
|
+
kind="extends", origin="manual"),
|
|
1086
|
+
],
|
|
1087
|
+
),
|
|
1088
|
+
),
|
|
1089
|
+
EvalTask(
|
|
1090
|
+
id="roadmap-dream-distill",
|
|
1091
|
+
name="Dream pass distills each community into one consolidator summary node",
|
|
1092
|
+
ingests=[
|
|
1093
|
+
("alpha beta gamma delta", {}),
|
|
1094
|
+
("alpha beta gamma epsilon", {}),
|
|
1095
|
+
("alpha beta gamma zeta", {}),
|
|
1096
|
+
("omega psi chi phi", {}),
|
|
1097
|
+
("omega psi chi kappa", {}),
|
|
1098
|
+
("omega psi chi lambda", {}),
|
|
1099
|
+
],
|
|
1100
|
+
actions=[_dream_distill],
|
|
1101
|
+
oracle=EvalOracle(
|
|
1102
|
+
# 6 members + 2 summary nodes (one per cluster)
|
|
1103
|
+
node_count=8,
|
|
1104
|
+
# 2 summaries x 3 members, all `origin="consolidator"`
|
|
1105
|
+
min_edges_by_origin={"consolidator": 6},
|
|
1106
|
+
),
|
|
1107
|
+
),
|
|
1108
|
+
# Welle C (2026-09-18): the agent-facing write path. An agent that consults
|
|
1109
|
+
# the brain must be able to leave something in it — under its own provenance
|
|
1110
|
+
# (`source="agent"` / `origin="agent"`), never destructively (`forget`
|
|
1111
|
+
# tombstones and invalidates, it never deletes), and only when the operator
|
|
1112
|
+
# opts in (`ig mcp --write`; the MCP default stays read-only).
|
|
1113
|
+
EvalTask(
|
|
1114
|
+
id="roadmap-agent-memory",
|
|
1115
|
+
name="Agent write path: remember marks provenance, forget tombstones without deleting",
|
|
1116
|
+
ingests=[
|
|
1117
|
+
("alpha beta gamma delta", {}),
|
|
1118
|
+
("omega psi chi phi", {}),
|
|
1119
|
+
],
|
|
1120
|
+
actions=[
|
|
1121
|
+
_agent_remember("a note an agent decided to keep", "alpha beta gamma delta"),
|
|
1122
|
+
_agent_forget("omega psi chi phi", reason="superseded by the newer note"),
|
|
1123
|
+
],
|
|
1124
|
+
oracle=EvalOracle(
|
|
1125
|
+
# 2 ingests + 1 remembered; the forgotten node is NEVER deleted
|
|
1126
|
+
node_count=3,
|
|
1127
|
+
nodes_present=["a note an agent decided to keep", "omega psi chi phi"],
|
|
1128
|
+
node_status={"omega psi chi phi": "tombstone"},
|
|
1129
|
+
# the agent-declared relation carries agent provenance
|
|
1130
|
+
min_edges_by_origin={"agent": 1},
|
|
1131
|
+
),
|
|
1132
|
+
),
|
|
1133
|
+
# Welle B/2 (2026-09-18): the status lifecycle — promotion and decay driven by
|
|
1134
|
+
# the recall signal. Gates are data-derived (see graph_engine/dream.py): the
|
|
1135
|
+
# case pins them explicitly so it stays readable.
|
|
1136
|
+
EvalTask(
|
|
1137
|
+
id="roadmap-dream-lifecycle",
|
|
1138
|
+
name="Lifecycle pass promotes what is used, demotes what is not, deletes nothing",
|
|
1139
|
+
ingests=[
|
|
1140
|
+
("alpha beta gamma delta", {}),
|
|
1141
|
+
("omega psi chi phi", {}),
|
|
1142
|
+
("kappa lambda my tau", {}),
|
|
1143
|
+
],
|
|
1144
|
+
actions=[
|
|
1145
|
+
_link_same_as("alpha beta gamma delta", "omega psi chi phi"),
|
|
1146
|
+
_link_same_as("alpha beta gamma delta", "kappa lambda my tau"),
|
|
1147
|
+
# alpha (degree 2) and kappa (degree 1) are recalled; omega is not
|
|
1148
|
+
_record_recall("what is used", ["alpha beta gamma delta", "kappa lambda my tau"]),
|
|
1149
|
+
_dream_refresh, # folds the ledger into recall_count
|
|
1150
|
+
# stale_days=0 so the decay gate can fire on a fresh fixture — the
|
|
1151
|
+
# live default is 30 days (a month of silence).
|
|
1152
|
+
_dream_lifecycle(),
|
|
1153
|
+
],
|
|
1154
|
+
oracle=EvalOracle(
|
|
1155
|
+
# nothing is ever deleted, not even a demoted node
|
|
1156
|
+
node_count=3,
|
|
1157
|
+
node_status={
|
|
1158
|
+
"alpha beta gamma delta": "active", # used + connected
|
|
1159
|
+
"omega psi chi phi": "stale", # unused + weak + old
|
|
1160
|
+
"kappa lambda my tau": "probation", # used, but not connected
|
|
1161
|
+
},
|
|
1162
|
+
),
|
|
1163
|
+
),
|
|
1164
|
+
EvalTask(
|
|
1165
|
+
id="episodic-auto-consolidation",
|
|
1166
|
+
name="Used episodes yield one semantic fact with both sources; repeats do no work",
|
|
1167
|
+
ingests=[],
|
|
1168
|
+
actions=[
|
|
1169
|
+
_seed_episodic_consolidation,
|
|
1170
|
+
_record_recall("reply preferences", ["User requested brief replies"]),
|
|
1171
|
+
_dream_refresh,
|
|
1172
|
+
_consolidate_episodic,
|
|
1173
|
+
_consolidate_episodic,
|
|
1174
|
+
],
|
|
1175
|
+
oracle=EvalOracle(
|
|
1176
|
+
node_count=4,
|
|
1177
|
+
nodes_present=["User requested brief replies", "User prefers concise answers",
|
|
1178
|
+
"A button was clicked"],
|
|
1179
|
+
node_status={"User prefers concise answers": "probation"},
|
|
1180
|
+
edges=[EdgeExpectation("User prefers concise answers",
|
|
1181
|
+
"User requested brief replies", "extends", pending=False)],
|
|
1182
|
+
no_edge=[EdgeExpectation("User prefers concise answers", "A button was clicked", "*")],
|
|
1183
|
+
min_edges_by_origin={"consolidator": 2},
|
|
1184
|
+
),
|
|
1185
|
+
),
|
|
1186
|
+
|
|
1187
|
+
]
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
# ---------------------------------------------------------------------------
|
|
1192
|
+
# Roadmap cases — desired future behavior (turns green once implemented)
|
|
1193
|
+
# ---------------------------------------------------------------------------
|
|
1194
|
+
|
|
1195
|
+
ROADMAP_CASES: list[EvalTask] = [
|
|
1196
|
+
# Admit-rule enforcement (V2#3) is implemented → GOLDEN_SET
|
|
1197
|
+
# (`admit-rule-no-relations`, `admit-rule-with-relations`).
|
|
1198
|
+
# Late chunking (V2#1) deliberately stays NO eval case: as long as there is
|
|
1199
|
+
# no real chunking layer (the engine embeds the whole node text), it cannot
|
|
1200
|
+
# be specified as an end-state oracle — a case would either turn
|
|
1201
|
+
# spuriously green (whole-text embedding satisfies it trivially) or red
|
|
1202
|
+
# for the wrong reasons. So it remains a documented roadmap note, not a case.
|
|
1203
|
+
#
|
|
1204
|
+
# Cross-encoder reranking (V2#1) is implemented → GOLDEN_SET
|
|
1205
|
+
# (`retrieval-rerank-honored`).
|
|
1206
|
+
]
|