recallnest 2.2.1 → 2.5.4
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.
- package/CHANGELOG.md +150 -0
- package/README.md +46 -35
- package/README_CN.md +41 -26
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +782 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +216 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +321 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +250 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +314 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +199 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/connector-types.ts +57 -0
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +126 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +38 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +234 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/obsidian-connector.ts +282 -0
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +3 -2
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/source-heartbeat.ts +108 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -30
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildGraph,
|
|
4
|
+
pprTraverse,
|
|
5
|
+
pprFromTriples,
|
|
6
|
+
edgeWeight,
|
|
7
|
+
DEFAULT_PPR_CONFIG,
|
|
8
|
+
} from "../ppr-traversal.js";
|
|
9
|
+
import type { KGTriple, NeighborhoodResult } from "../kg-store.js";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Helpers
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
function makeTriple(
|
|
16
|
+
subject: string,
|
|
17
|
+
predicate: string,
|
|
18
|
+
object: string,
|
|
19
|
+
confidence = 0.9,
|
|
20
|
+
id?: string,
|
|
21
|
+
): KGTriple {
|
|
22
|
+
return {
|
|
23
|
+
id: id ?? `${subject}-${predicate}-${object}`,
|
|
24
|
+
scope: "test",
|
|
25
|
+
subject,
|
|
26
|
+
predicate,
|
|
27
|
+
object,
|
|
28
|
+
confidence,
|
|
29
|
+
source_memory_id: `mem-${subject}-${object}`,
|
|
30
|
+
source_text: `${subject} ${predicate} ${object}`,
|
|
31
|
+
timestamp: Date.now(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeNeighborhood(triples: KGTriple[]): NeighborhoodResult[] {
|
|
36
|
+
const entities = new Set<string>();
|
|
37
|
+
for (const t of triples) {
|
|
38
|
+
entities.add(t.subject);
|
|
39
|
+
entities.add(t.object);
|
|
40
|
+
}
|
|
41
|
+
return [...entities].map(entity => ({
|
|
42
|
+
entity,
|
|
43
|
+
triples: triples.filter(t => t.subject === entity || t.object === entity),
|
|
44
|
+
hops: 0,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// edgeWeight
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
describe("edgeWeight", () => {
|
|
53
|
+
it("returns known weights for standard predicates", () => {
|
|
54
|
+
expect(edgeWeight("created_by")).toBe(0.95);
|
|
55
|
+
expect(edgeWeight("uses")).toBe(0.90);
|
|
56
|
+
expect(edgeWeight("related_to")).toBe(0.50);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns default weight for unknown predicates", () => {
|
|
60
|
+
expect(edgeWeight("unknown_predicate")).toBe(0.60);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// buildGraph
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
describe("buildGraph", () => {
|
|
69
|
+
it("builds undirected graph from neighborhood", () => {
|
|
70
|
+
const triples = [
|
|
71
|
+
makeTriple("Alice", "uses", "Python"),
|
|
72
|
+
makeTriple("Alice", "works_with", "Bob"),
|
|
73
|
+
];
|
|
74
|
+
const neighborhood = makeNeighborhood(triples);
|
|
75
|
+
const graph = buildGraph(neighborhood);
|
|
76
|
+
|
|
77
|
+
expect(graph.nodes.size).toBe(3); // Alice, Python, Bob
|
|
78
|
+
expect(graph.adj.get("Alice")?.length).toBe(2); // -> Python, -> Bob
|
|
79
|
+
expect(graph.adj.get("Python")?.length).toBe(1); // -> Alice
|
|
80
|
+
expect(graph.adj.get("Bob")?.length).toBe(1); // -> Alice
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("deduplicates triples by id", () => {
|
|
84
|
+
const t = makeTriple("Alice", "uses", "Python", 0.9, "same-id");
|
|
85
|
+
const neighborhood: NeighborhoodResult[] = [
|
|
86
|
+
{ entity: "Alice", triples: [t], hops: 0 },
|
|
87
|
+
{ entity: "Python", triples: [t], hops: 1 },
|
|
88
|
+
];
|
|
89
|
+
const graph = buildGraph(neighborhood);
|
|
90
|
+
|
|
91
|
+
// Should only have 1 edge per direction despite appearing in 2 neighborhood entries
|
|
92
|
+
expect(graph.adj.get("Alice")?.length).toBe(1);
|
|
93
|
+
expect(graph.adj.get("Python")?.length).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("handles empty neighborhood", () => {
|
|
97
|
+
const graph = buildGraph([]);
|
|
98
|
+
expect(graph.nodes.size).toBe(0);
|
|
99
|
+
expect(graph.adj.size).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// pprTraverse — convergence
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
describe("pprTraverse", () => {
|
|
108
|
+
it("converges on a simple chain A -> B -> C", () => {
|
|
109
|
+
const triples = [
|
|
110
|
+
makeTriple("A", "uses", "B"),
|
|
111
|
+
makeTriple("B", "uses", "C"),
|
|
112
|
+
];
|
|
113
|
+
const graph = buildGraph(makeNeighborhood(triples));
|
|
114
|
+
const results = pprTraverse(graph, ["A"]);
|
|
115
|
+
|
|
116
|
+
// A should have highest score (it's the seed)
|
|
117
|
+
expect(results.length).toBeGreaterThan(0);
|
|
118
|
+
expect(results[0].entity).toBe("A");
|
|
119
|
+
|
|
120
|
+
// B should score higher than C (closer to seed)
|
|
121
|
+
const bResult = results.find(r => r.entity === "B");
|
|
122
|
+
const cResult = results.find(r => r.entity === "C");
|
|
123
|
+
expect(bResult).toBeDefined();
|
|
124
|
+
expect(cResult).toBeDefined();
|
|
125
|
+
expect(bResult!.score).toBeGreaterThan(cResult!.score);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("respects hop limits", () => {
|
|
129
|
+
const triples = [
|
|
130
|
+
makeTriple("A", "uses", "B"),
|
|
131
|
+
makeTriple("B", "uses", "C"),
|
|
132
|
+
makeTriple("C", "uses", "D"),
|
|
133
|
+
];
|
|
134
|
+
const graph = buildGraph(makeNeighborhood(triples));
|
|
135
|
+
const results = pprTraverse(graph, ["A"], { hopLimit: 1 });
|
|
136
|
+
|
|
137
|
+
// With hopLimit=1, only A and B should be reachable
|
|
138
|
+
const entities = results.map(r => r.entity);
|
|
139
|
+
expect(entities).toContain("A");
|
|
140
|
+
expect(entities).toContain("B");
|
|
141
|
+
expect(entities).not.toContain("C");
|
|
142
|
+
expect(entities).not.toContain("D");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("handles multiple seed entities", () => {
|
|
146
|
+
const triples = [
|
|
147
|
+
makeTriple("A", "uses", "C"),
|
|
148
|
+
makeTriple("B", "uses", "C"),
|
|
149
|
+
];
|
|
150
|
+
const graph = buildGraph(makeNeighborhood(triples));
|
|
151
|
+
const results = pprTraverse(graph, ["A", "B"]);
|
|
152
|
+
|
|
153
|
+
// C should be reachable from both seeds
|
|
154
|
+
const cResult = results.find(r => r.entity === "C");
|
|
155
|
+
expect(cResult).toBeDefined();
|
|
156
|
+
expect(cResult!.hops).toBe(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("returns empty for empty graph", () => {
|
|
160
|
+
const graph = buildGraph([]);
|
|
161
|
+
const results = pprTraverse(graph, ["A"]);
|
|
162
|
+
expect(results).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("returns empty when seeds not in graph", () => {
|
|
166
|
+
const triples = [makeTriple("X", "uses", "Y")];
|
|
167
|
+
const graph = buildGraph(makeNeighborhood(triples));
|
|
168
|
+
const results = pprTraverse(graph, ["NotInGraph"]);
|
|
169
|
+
expect(results).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("respects topK limit", () => {
|
|
173
|
+
const triples = [
|
|
174
|
+
makeTriple("A", "uses", "B"),
|
|
175
|
+
makeTriple("A", "uses", "C"),
|
|
176
|
+
makeTriple("A", "uses", "D"),
|
|
177
|
+
makeTriple("A", "uses", "E"),
|
|
178
|
+
];
|
|
179
|
+
const graph = buildGraph(makeNeighborhood(triples));
|
|
180
|
+
const results = pprTraverse(graph, ["A"], { topK: 2 });
|
|
181
|
+
expect(results.length).toBe(2);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("computes shortest paths correctly", () => {
|
|
185
|
+
const triples = [
|
|
186
|
+
makeTriple("A", "uses", "B"),
|
|
187
|
+
makeTriple("B", "uses", "C"),
|
|
188
|
+
];
|
|
189
|
+
const graph = buildGraph(makeNeighborhood(triples));
|
|
190
|
+
const results = pprTraverse(graph, ["A"]);
|
|
191
|
+
|
|
192
|
+
const aResult = results.find(r => r.entity === "A");
|
|
193
|
+
const bResult = results.find(r => r.entity === "B");
|
|
194
|
+
const cResult = results.find(r => r.entity === "C");
|
|
195
|
+
|
|
196
|
+
expect(aResult?.hops).toBe(0);
|
|
197
|
+
expect(aResult?.path).toEqual(["A"]);
|
|
198
|
+
expect(bResult?.hops).toBe(1);
|
|
199
|
+
expect(bResult?.path).toEqual(["A", "B"]);
|
|
200
|
+
expect(cResult?.hops).toBe(2);
|
|
201
|
+
expect(cResult?.path).toEqual(["A", "B", "C"]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("edge weights affect scores — stronger predicates yield higher scores", () => {
|
|
205
|
+
// Two separate subgraphs from same seed
|
|
206
|
+
const triples = [
|
|
207
|
+
makeTriple("A", "created_by", "B", 1.0), // weight 0.95
|
|
208
|
+
makeTriple("A", "related_to", "C", 1.0), // weight 0.50
|
|
209
|
+
];
|
|
210
|
+
const graph = buildGraph(makeNeighborhood(triples));
|
|
211
|
+
const results = pprTraverse(graph, ["A"]);
|
|
212
|
+
|
|
213
|
+
const bResult = results.find(r => r.entity === "B");
|
|
214
|
+
const cResult = results.find(r => r.entity === "C");
|
|
215
|
+
expect(bResult).toBeDefined();
|
|
216
|
+
expect(cResult).toBeDefined();
|
|
217
|
+
// B should score higher due to stronger edge weight
|
|
218
|
+
expect(bResult!.score).toBeGreaterThan(cResult!.score);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("all scores sum to approximately 1.0 (probability distribution)", () => {
|
|
222
|
+
const triples = [
|
|
223
|
+
makeTriple("A", "uses", "B"),
|
|
224
|
+
makeTriple("B", "uses", "C"),
|
|
225
|
+
makeTriple("C", "uses", "A"), // cycle
|
|
226
|
+
];
|
|
227
|
+
const graph = buildGraph(makeNeighborhood(triples));
|
|
228
|
+
const results = pprTraverse(graph, ["A"], { maxIterations: 50 });
|
|
229
|
+
|
|
230
|
+
const totalScore = results.reduce((sum, r) => sum + r.score, 0);
|
|
231
|
+
// PPR scores approximate a probability distribution
|
|
232
|
+
expect(totalScore).toBeGreaterThan(0.8);
|
|
233
|
+
expect(totalScore).toBeLessThan(1.2);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// pprFromTriples (convenience)
|
|
239
|
+
// ============================================================================
|
|
240
|
+
|
|
241
|
+
describe("pprFromTriples", () => {
|
|
242
|
+
it("works with raw triples", () => {
|
|
243
|
+
const triples = [
|
|
244
|
+
makeTriple("Alice", "uses", "Python"),
|
|
245
|
+
makeTriple("Alice", "works_at", "Google"),
|
|
246
|
+
];
|
|
247
|
+
const results = pprFromTriples(triples, ["Alice"]);
|
|
248
|
+
|
|
249
|
+
expect(results.length).toBeGreaterThan(0);
|
|
250
|
+
expect(results[0].entity).toBe("Alice");
|
|
251
|
+
|
|
252
|
+
const entities = results.map(r => r.entity);
|
|
253
|
+
expect(entities).toContain("Python");
|
|
254
|
+
expect(entities).toContain("Google");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for HP-predictive Phase 5: Prediction Engine
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* 1. collectSignals() — each signal type is correctly extracted
|
|
6
|
+
* 2. scorePredictions() — scoring, filtering by threshold, ranking
|
|
7
|
+
* 3. Edge cases — empty inputs, boundary conditions
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
collectSignals,
|
|
12
|
+
scorePredictions,
|
|
13
|
+
_testConstants,
|
|
14
|
+
type PredictionContext,
|
|
15
|
+
type PredictionSignal,
|
|
16
|
+
} from "../prediction-engine.js";
|
|
17
|
+
import type { SessionCheckpointRecord } from "../session-schema.js";
|
|
18
|
+
import type { WorkflowObservationRecord } from "../workflow-observation-schema.js";
|
|
19
|
+
|
|
20
|
+
const { CONFIDENCE_THRESHOLD, STALE_OPEN_LOOP_HOURS, STALE_MEMORY_DAYS, MAX_PREDICTIONS, SIGNAL_WEIGHTS } = _testConstants;
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function makeCheckpoint(overrides: Partial<SessionCheckpointRecord> & { openLoops?: string[]; updatedAt: string }): SessionCheckpointRecord {
|
|
27
|
+
return {
|
|
28
|
+
checkpointId: `cp-${Math.random().toString(36).slice(2, 8)}`,
|
|
29
|
+
sessionId: "test-session",
|
|
30
|
+
resolvedScope: "test",
|
|
31
|
+
scope: "test",
|
|
32
|
+
summary: "test checkpoint",
|
|
33
|
+
task: "test task",
|
|
34
|
+
decisions: [],
|
|
35
|
+
openLoops: overrides.openLoops ?? [],
|
|
36
|
+
nextActions: [],
|
|
37
|
+
entities: [],
|
|
38
|
+
files: [],
|
|
39
|
+
updatedAt: overrides.updatedAt,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeWorkflowObservation(overrides: Partial<WorkflowObservationRecord>): WorkflowObservationRecord {
|
|
45
|
+
return {
|
|
46
|
+
observationId: `obs-${Math.random().toString(36).slice(2, 8)}`,
|
|
47
|
+
workflowId: "test-workflow",
|
|
48
|
+
outcome: "corrected",
|
|
49
|
+
summary: "Fixed a workflow issue",
|
|
50
|
+
scope: "test",
|
|
51
|
+
resolvedScope: "test",
|
|
52
|
+
source: "manual",
|
|
53
|
+
tags: [],
|
|
54
|
+
tools: [],
|
|
55
|
+
recordedAt: new Date().toISOString(),
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function emptyContext(overrides?: Partial<PredictionContext>): PredictionContext {
|
|
61
|
+
return {
|
|
62
|
+
checkpoints: [],
|
|
63
|
+
workflowObservations: [],
|
|
64
|
+
frequentMemories: [],
|
|
65
|
+
uncoveredTopics: [],
|
|
66
|
+
now: new Date("2026-04-11T12:00:00Z"),
|
|
67
|
+
...overrides,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// collectSignals
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
describe("collectSignals", () => {
|
|
76
|
+
describe("stale open loop signals", () => {
|
|
77
|
+
it("detects stale open loops from checkpoints older than 24h", () => {
|
|
78
|
+
const now = new Date("2026-04-11T12:00:00Z");
|
|
79
|
+
const staleCheckpoint = makeCheckpoint({
|
|
80
|
+
openLoops: ["Fix CI pipeline", "Review PR #42"],
|
|
81
|
+
updatedAt: new Date(now.getTime() - 48 * 60 * 60 * 1000).toISOString(), // 48h ago
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const signals = collectSignals(emptyContext({
|
|
85
|
+
checkpoints: [staleCheckpoint],
|
|
86
|
+
now,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
const openLoopSignals = signals.filter(s => s.type === "stale_open_loop");
|
|
90
|
+
expect(openLoopSignals.length).toBe(2);
|
|
91
|
+
expect(openLoopSignals[0].trigger).toBe("Fix CI pipeline");
|
|
92
|
+
expect(openLoopSignals[0].weight).toBeGreaterThan(0.6);
|
|
93
|
+
expect(openLoopSignals[0].evidence.length).toBeGreaterThan(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("ignores fresh open loops (< 24h)", () => {
|
|
97
|
+
const now = new Date("2026-04-11T12:00:00Z");
|
|
98
|
+
const freshCheckpoint = makeCheckpoint({
|
|
99
|
+
openLoops: ["In-progress task"],
|
|
100
|
+
updatedAt: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(), // 12h ago
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const signals = collectSignals(emptyContext({
|
|
104
|
+
checkpoints: [freshCheckpoint],
|
|
105
|
+
now,
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
expect(signals.filter(s => s.type === "stale_open_loop").length).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("deduplicates same open loop across checkpoints", () => {
|
|
112
|
+
const now = new Date("2026-04-11T12:00:00Z");
|
|
113
|
+
const cp1 = makeCheckpoint({
|
|
114
|
+
openLoops: ["Fix CI pipeline"],
|
|
115
|
+
updatedAt: new Date(now.getTime() - 48 * 60 * 60 * 1000).toISOString(),
|
|
116
|
+
});
|
|
117
|
+
const cp2 = makeCheckpoint({
|
|
118
|
+
openLoops: ["fix ci pipeline"], // same but different case
|
|
119
|
+
updatedAt: new Date(now.getTime() - 72 * 60 * 60 * 1000).toISOString(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const signals = collectSignals(emptyContext({
|
|
123
|
+
checkpoints: [cp1, cp2],
|
|
124
|
+
now,
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
expect(signals.filter(s => s.type === "stale_open_loop").length).toBe(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("weights increase with staleness", () => {
|
|
131
|
+
const now = new Date("2026-04-11T12:00:00Z");
|
|
132
|
+
const cp24h = makeCheckpoint({
|
|
133
|
+
openLoops: ["Task A"],
|
|
134
|
+
updatedAt: new Date(now.getTime() - 25 * 60 * 60 * 1000).toISOString(),
|
|
135
|
+
});
|
|
136
|
+
const cp72h = makeCheckpoint({
|
|
137
|
+
openLoops: ["Task B"],
|
|
138
|
+
updatedAt: new Date(now.getTime() - 72 * 60 * 60 * 1000).toISOString(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const signals = collectSignals(emptyContext({
|
|
142
|
+
checkpoints: [cp24h, cp72h],
|
|
143
|
+
now,
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
const signalA = signals.find(s => s.trigger === "Task A")!;
|
|
147
|
+
const signalB = signals.find(s => s.trigger === "Task B")!;
|
|
148
|
+
expect(signalB.weight).toBeGreaterThan(signalA.weight);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("stale high-frequency memory signals", () => {
|
|
153
|
+
it("detects memories accessed frequently but not recently", () => {
|
|
154
|
+
const now = new Date("2026-04-11T12:00:00Z");
|
|
155
|
+
const signals = collectSignals(emptyContext({
|
|
156
|
+
frequentMemories: [{
|
|
157
|
+
text: "Auth migration plan",
|
|
158
|
+
topicTag: "auth",
|
|
159
|
+
lastAccessedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days ago
|
|
160
|
+
accessCount: 5,
|
|
161
|
+
}],
|
|
162
|
+
now,
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
const staleSignals = signals.filter(s => s.type === "stale_high_frequency");
|
|
166
|
+
expect(staleSignals.length).toBe(1);
|
|
167
|
+
expect(staleSignals[0].trigger).toBe("auth");
|
|
168
|
+
expect(staleSignals[0].evidence).toContain("Accessed 5 times but not in 30 days");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("ignores recently accessed memories", () => {
|
|
172
|
+
const now = new Date("2026-04-11T12:00:00Z");
|
|
173
|
+
const signals = collectSignals(emptyContext({
|
|
174
|
+
frequentMemories: [{
|
|
175
|
+
text: "Recent memory",
|
|
176
|
+
lastAccessedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
|
|
177
|
+
accessCount: 10,
|
|
178
|
+
}],
|
|
179
|
+
now,
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
expect(signals.filter(s => s.type === "stale_high_frequency").length).toBe(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("ignores infrequently accessed memories (< 3 accesses)", () => {
|
|
186
|
+
const now = new Date("2026-04-11T12:00:00Z");
|
|
187
|
+
const signals = collectSignals(emptyContext({
|
|
188
|
+
frequentMemories: [{
|
|
189
|
+
text: "Rarely accessed",
|
|
190
|
+
lastAccessedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
191
|
+
accessCount: 2,
|
|
192
|
+
}],
|
|
193
|
+
now,
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
expect(signals.filter(s => s.type === "stale_high_frequency").length).toBe(0);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("workflow issue signals", () => {
|
|
201
|
+
it("detects corrected/missed workflow observations", () => {
|
|
202
|
+
const observations = [
|
|
203
|
+
makeWorkflowObservation({ workflowId: "deploy", outcome: "corrected", summary: "Forgot to run tests" }),
|
|
204
|
+
makeWorkflowObservation({ workflowId: "deploy", outcome: "missed", summary: "Missed the changelog update" }),
|
|
205
|
+
makeWorkflowObservation({ workflowId: "deploy", outcome: "success", summary: "All good" }),
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const signals = collectSignals(emptyContext({ workflowObservations: observations }));
|
|
209
|
+
const issueSignals = signals.filter(s => s.type === "workflow_issue");
|
|
210
|
+
expect(issueSignals.length).toBe(1);
|
|
211
|
+
expect(issueSignals[0].trigger).toBe("deploy");
|
|
212
|
+
expect(issueSignals[0].evidence[0]).toContain("2 corrected/missed");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("ignores workflows with only successes", () => {
|
|
216
|
+
const observations = [
|
|
217
|
+
makeWorkflowObservation({ workflowId: "deploy", outcome: "success" }),
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const signals = collectSignals(emptyContext({ workflowObservations: observations }));
|
|
221
|
+
expect(signals.filter(s => s.type === "workflow_issue").length).toBe(0);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("uncovered topic signals", () => {
|
|
226
|
+
it("creates signals for uncovered topics", () => {
|
|
227
|
+
const signals = collectSignals(emptyContext({
|
|
228
|
+
uncoveredTopics: ["kubernetes", "migration"],
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
const topicSignals = signals.filter(s => s.type === "uncovered_topic");
|
|
232
|
+
expect(topicSignals.length).toBe(2);
|
|
233
|
+
expect(topicSignals[0].trigger).toBe("kubernetes");
|
|
234
|
+
expect(topicSignals[0].weight).toBe(0.65);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("empty inputs", () => {
|
|
239
|
+
it("returns empty array for empty context", () => {
|
|
240
|
+
const signals = collectSignals(emptyContext());
|
|
241
|
+
expect(signals).toEqual([]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// scorePredictions
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
describe("scorePredictions", () => {
|
|
251
|
+
it("scores signals and filters by confidence threshold", () => {
|
|
252
|
+
const signals: PredictionSignal[] = [
|
|
253
|
+
{
|
|
254
|
+
type: "stale_open_loop",
|
|
255
|
+
trigger: "Fix CI",
|
|
256
|
+
action: "Unresolved: Fix CI",
|
|
257
|
+
weight: 0.9,
|
|
258
|
+
evidence: ["48h old"],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
type: "uncovered_topic",
|
|
262
|
+
trigger: "obscure",
|
|
263
|
+
action: "No memories for obscure",
|
|
264
|
+
weight: 0.5, // Below threshold after scoring
|
|
265
|
+
evidence: ["No coverage"],
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
const predictions = scorePredictions(signals);
|
|
270
|
+
|
|
271
|
+
// stale_open_loop: 0.9 * 0.9 = 0.81 (above 0.6)
|
|
272
|
+
// uncovered_topic: 0.5 * 0.65 = 0.325 (below 0.6)
|
|
273
|
+
expect(predictions.length).toBe(1);
|
|
274
|
+
expect(predictions[0].trigger).toBe("Fix CI");
|
|
275
|
+
expect(predictions[0].confidence).toBe(0.81);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("sorts by confidence descending", () => {
|
|
279
|
+
const signals: PredictionSignal[] = [
|
|
280
|
+
{ type: "uncovered_topic", trigger: "low", action: "low", weight: 1.0, evidence: [] },
|
|
281
|
+
{ type: "stale_open_loop", trigger: "high", action: "high", weight: 1.0, evidence: [] },
|
|
282
|
+
{ type: "workflow_issue", trigger: "mid", action: "mid", weight: 1.0, evidence: [] },
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
const predictions = scorePredictions(signals);
|
|
286
|
+
// stale_open_loop: 1.0 * 0.9 = 0.9
|
|
287
|
+
// workflow_issue: 1.0 * 0.85 = 0.85
|
|
288
|
+
// uncovered_topic: 1.0 * 0.65 = 0.65
|
|
289
|
+
expect(predictions.length).toBe(3);
|
|
290
|
+
expect(predictions[0].trigger).toBe("high");
|
|
291
|
+
expect(predictions[1].trigger).toBe("mid");
|
|
292
|
+
expect(predictions[2].trigger).toBe("low");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("limits to MAX_PREDICTIONS", () => {
|
|
296
|
+
const signals: PredictionSignal[] = Array.from({ length: 10 }, (_, i) => ({
|
|
297
|
+
type: "stale_open_loop" as const,
|
|
298
|
+
trigger: `task-${i}`,
|
|
299
|
+
action: `action-${i}`,
|
|
300
|
+
weight: 1.0,
|
|
301
|
+
evidence: [],
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
const predictions = scorePredictions(signals);
|
|
305
|
+
expect(predictions.length).toBe(MAX_PREDICTIONS);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("returns empty for all below threshold", () => {
|
|
309
|
+
const signals: PredictionSignal[] = [
|
|
310
|
+
{ type: "uncovered_topic", trigger: "x", action: "x", weight: 0.3, evidence: [] },
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
const predictions = scorePredictions(signals);
|
|
314
|
+
expect(predictions.length).toBe(0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("returns empty for empty signals", () => {
|
|
318
|
+
expect(scorePredictions([])).toEqual([]);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Constants sanity
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
describe("prediction engine constants", () => {
|
|
327
|
+
it("confidence threshold is 0.6", () => {
|
|
328
|
+
expect(CONFIDENCE_THRESHOLD).toBe(0.6);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("stale open loop hours is 24", () => {
|
|
332
|
+
expect(STALE_OPEN_LOOP_HOURS).toBe(24);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("stale memory days is 14", () => {
|
|
336
|
+
expect(STALE_MEMORY_DAYS).toBe(14);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("max predictions is 5", () => {
|
|
340
|
+
expect(MAX_PREDICTIONS).toBe(5);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("signal weights are all between 0 and 1", () => {
|
|
344
|
+
for (const [, weight] of Object.entries(SIGNAL_WEIGHTS)) {
|
|
345
|
+
expect(weight).toBeGreaterThanOrEqual(0);
|
|
346
|
+
expect(weight).toBeLessThanOrEqual(1);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|