recallnest 1.4.1
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/.claude-plugin/CLAUDE.md +29 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +28 -0
- package/.mcp.json +9 -0
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +42 -0
- package/DEMO.md +94 -0
- package/EVAL.md +89 -0
- package/FAILURES.md +75 -0
- package/LICENSE +21 -0
- package/OPERATIONS.md +59 -0
- package/README.md +427 -0
- package/README_CN.md +425 -0
- package/ROADMAP.md +198 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +807 -0
- package/assets/ui/index.html +154 -0
- package/assets/ui/styles.css +605 -0
- package/assets/wechat-qr.jpg +0 -0
- package/config.json.example +38 -0
- package/daily-recall.sh +87 -0
- package/daily-reflections/2026-03-05.md +15 -0
- package/daily-reflections/2026-03-06.md +15 -0
- package/daily-reflections/2026-03-07.md +7 -0
- package/daily-reflections/2026-03-08.md +34 -0
- package/daily-reflections/2026-03-09.md +35 -0
- package/daily-reflections/2026-03-10.md +36 -0
- package/daily-reflections/2026-03-11.md +35 -0
- package/daily-reflections/2026-03-12.md +36 -0
- package/daily-reflections/2026-03-13.md +35 -0
- package/daily-reflections/2026-03-14.md +35 -0
- package/daily-reflections/2026-03-15.md +35 -0
- package/daily-reflections/2026-03-16.md +41 -0
- package/daily-reflections/2026-03-17.md +10 -0
- package/daily-reflections/2026-03-18.md +10 -0
- package/daily-reflections/2026-03-19.md +10 -0
- package/daily-reflections/2026-03-20.md +10 -0
- package/daily-reflections/2026-03-21.md +10 -0
- package/daily-reflections/2026-03-22.md +10 -0
- package/daily-reflections/2026-03-23.md +10 -0
- package/daily-reflections/2026-03-24.md +10 -0
- package/daily-reflections/2026-03-25.md +32 -0
- package/daily-reflections/2026-03-26.md +37 -0
- package/daily-reflections/2026-03-27.md +35 -0
- package/daily-reflections/2026-03-28.md +35 -0
- package/daily-reflections/2026-03-29.md +35 -0
- package/daily-reflections/2026-03-30.md +36 -0
- package/daily-reflections/2026-03-31.md +35 -0
- package/daily-reflections/2026-04-01.md +35 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/memory-boundary-contract.md +226 -0
- package/docs/memory-categories.md +119 -0
- package/docs/pipeline-audit-ultramemory.md +177 -0
- package/docs/plans/2026-03-08-query-expansion.md +270 -0
- package/docs/plans/2026-03-16-continuity-layer.md +196 -0
- package/docs/plans/mcp-tiering-plan.md +126 -0
- package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
- package/docs/self-evolution.md +198 -0
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
- package/eval/cases.json +85 -0
- package/eval/continuity/case-seeds.json +90 -0
- package/eval/continuity/cases.json +302 -0
- package/eval/continuity/memory-seeds.json +26 -0
- package/eval/continuity/pattern-seeds.json +100 -0
- package/eval/continuity/results-2026-03-16.md +117 -0
- package/eval/continuity/results-2026-03-19.md +453 -0
- package/eval/continuity/runbook.md +110 -0
- package/eval/continuity/seed-cases.md +54 -0
- package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
- package/eval/reports/2026-03-06-baseline.md +82 -0
- package/eval/reports/query-expansion.md +138 -0
- package/integrations/README.md +57 -0
- package/integrations/claude-code/README.md +133 -0
- package/integrations/claude-code/claude-md-snippet.md +31 -0
- package/integrations/claude-code/setup.sh +66 -0
- package/integrations/claude-code/smoke-continuity.sh +452 -0
- package/integrations/codex/README.md +49 -0
- package/integrations/codex/agents-md-snippet.md +31 -0
- package/integrations/codex/setup.sh +58 -0
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
- package/integrations/examples/langchain/memory-chain.py +111 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
- package/integrations/gemini-cli/README.md +57 -0
- package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
- package/integrations/gemini-cli/setup.sh +64 -0
- package/integrations/shared/install-managed-block.sh +51 -0
- package/lm +19 -0
- package/package.json +50 -0
- package/plan-v1.2-distributable.md +105 -0
- package/scripts/archive-distilled-junk.ts +116 -0
- package/scripts/backfill-l0l1l2.ts +98 -0
- package/scripts/blind-scan.ts +588 -0
- package/scripts/cleanup-step1-exact-dups.ts +117 -0
- package/scripts/cleanup-step2-template-dups.ts +113 -0
- package/scripts/cleanup-step3-stale-content.ts +149 -0
- package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
- package/scripts/delete-distilled-junk.ts +88 -0
- package/scripts/distill-facts.ts +305 -0
- package/scripts/health-check.ts +343 -0
- package/scripts/incremental-ingest.sh +71 -0
- package/scripts/mlp-health-check.ts +382 -0
- package/scripts/pull-claude-desktop.ts +515 -0
- package/scripts/re-ingest-cc.sh +66 -0
- package/scripts/rebalance-memory-health.ts +181 -0
- package/scripts/run-desktop-ingest.sh +11 -0
- package/scripts/start-server.sh +12 -0
- package/scripts/viz-dashboard.html +423 -0
- package/scripts/viz-export.ts +703 -0
- package/src/__tests__/access-tracker-novelty.test.ts +228 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/capture-engine.test.ts +1142 -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__/confidence-tracker.test.ts +196 -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__/consolidation-engine.test.ts +181 -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-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__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/doctor.test.ts +303 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/kg-extractor.test.ts +184 -0
- package/src/__tests__/kg-store.test.ts +198 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/memory-boundaries.test.ts +237 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -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 +106 -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.test.ts +34 -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 +4743 -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__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/session-checkpoint.test.ts +259 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-output-compressor.test.ts +133 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation.test.ts +400 -0
- package/src/access-tracker.ts +316 -0
- package/src/adaptive-retrieval.ts +64 -0
- package/src/api-server.ts +667 -0
- package/src/asset-sync.ts +79 -0
- package/src/auto-consolidation.ts +133 -0
- package/src/auto-gc.ts +124 -0
- package/src/auto-recall.ts +95 -0
- package/src/capture-engine.ts +931 -0
- package/src/capture-heuristic.ts +177 -0
- package/src/cascade-forget.ts +135 -0
- package/src/chunker.ts +253 -0
- package/src/cli.ts +1917 -0
- package/src/confidence-tracker.ts +166 -0
- package/src/conflict-advisor.ts +433 -0
- package/src/conflict-engine.ts +181 -0
- package/src/conflict-escalation.ts +138 -0
- package/src/conflict-lifecycle.ts +67 -0
- package/src/conflict-output.ts +218 -0
- package/src/conflict-schema.ts +130 -0
- package/src/conflict-store.ts +127 -0
- package/src/consolidation-engine.ts +256 -0
- package/src/context-composer-pins.ts +183 -0
- package/src/context-composer-queries.ts +121 -0
- package/src/context-composer-scope.ts +56 -0
- package/src/context-composer-stable-selection.ts +200 -0
- package/src/context-composer-stable.ts +217 -0
- package/src/context-composer-task-fallbacks.ts +36 -0
- package/src/context-composer-task-ranking.ts +197 -0
- package/src/context-composer-task-results.ts +137 -0
- package/src/context-composer-task-selection.ts +342 -0
- package/src/context-composer-text.ts +57 -0
- package/src/context-composer.ts +300 -0
- package/src/context-renderer.ts +179 -0
- package/src/decay-engine.ts +189 -0
- package/src/doctor.ts +543 -0
- package/src/embedder.ts +557 -0
- package/src/entity-resolver.ts +131 -0
- package/src/eval.ts +632 -0
- package/src/event-segmenter.ts +216 -0
- package/src/ingest.ts +1822 -0
- package/src/kg-extractor.ts +217 -0
- package/src/kg-store.ts +353 -0
- package/src/legacy-scope-review.ts +53 -0
- package/src/llm-client.ts +629 -0
- package/src/llm-consolidation.ts +168 -0
- package/src/mcp-server.ts +1337 -0
- package/src/memory-assets.ts +475 -0
- package/src/memory-boundaries.ts +354 -0
- package/src/memory-health-rebalance.ts +220 -0
- package/src/memory-output.ts +474 -0
- package/src/memory-schema.ts +184 -0
- package/src/multi-vector.ts +234 -0
- package/src/noise-filter.ts +155 -0
- package/src/ppr-traversal.ts +279 -0
- package/src/preference-matcher.ts +174 -0
- package/src/preference-slots.ts +259 -0
- package/src/prospective-memory.ts +198 -0
- package/src/query-entity-detector.ts +207 -0
- package/src/query-expander.ts +98 -0
- package/src/result-synthesizer.ts +114 -0
- package/src/retrieval-profiles.ts +101 -0
- package/src/retrieval-trace.ts +94 -0
- package/src/retriever.ts +1338 -0
- package/src/retroactive-boost.ts +154 -0
- package/src/rif.ts +73 -0
- package/src/runtime-config.ts +172 -0
- package/src/schema-utils.ts +56 -0
- package/src/scope-inventory.ts +310 -0
- package/src/scope-policy.ts +142 -0
- package/src/session-engine.ts +168 -0
- package/src/session-output.ts +96 -0
- package/src/session-schema.ts +60 -0
- package/src/session-store.ts +293 -0
- package/src/stderr-log.ts +16 -0
- package/src/store.ts +903 -0
- package/src/temporal-parser.ts +318 -0
- package/src/term-registry.ts +791 -0
- package/src/tool-output-compressor.ts +115 -0
- package/src/tracker.ts +57 -0
- package/src/ui-server.ts +430 -0
- package/src/version-manager.ts +172 -0
- package/src/workflow-observation-engine.ts +316 -0
- package/src/workflow-observation-managed.ts +83 -0
- package/src/workflow-observation-output.ts +86 -0
- package/src/workflow-observation-schema.ts +87 -0
- package/src/workflow-observation-scope-review.ts +409 -0
- package/src/workflow-observation-store.ts +69 -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,211 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { promoteMemory } from "../capture-engine.js";
|
|
4
|
+
import { formatExplainResults, formatSearchResults } from "../memory-output.js";
|
|
5
|
+
import type { RetrievalResult } from "../retriever.js";
|
|
6
|
+
|
|
7
|
+
const TEST_SCOPE = "project:test";
|
|
8
|
+
|
|
9
|
+
function createDeps() {
|
|
10
|
+
const storedEntries: any[] = [];
|
|
11
|
+
const conflicts: any[] = [];
|
|
12
|
+
let seq = 1;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
storedEntries,
|
|
16
|
+
conflicts,
|
|
17
|
+
deps: {
|
|
18
|
+
embedder: {
|
|
19
|
+
async embedPassage(text: string) {
|
|
20
|
+
return [text.length, 1, 0];
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
store: {
|
|
24
|
+
async store(entry: any) {
|
|
25
|
+
const stored = {
|
|
26
|
+
...entry,
|
|
27
|
+
id: `00000000-0000-0000-0000-${String(seq).padStart(12, "0")}`,
|
|
28
|
+
timestamp: 1_700_000_000_000 + seq,
|
|
29
|
+
};
|
|
30
|
+
seq += 1;
|
|
31
|
+
storedEntries.push(stored);
|
|
32
|
+
return stored;
|
|
33
|
+
},
|
|
34
|
+
async list(_scopeFilter?: string[], category?: string, limit = 20, offset = 0) {
|
|
35
|
+
return storedEntries
|
|
36
|
+
.filter((entry) => !category || entry.category === category)
|
|
37
|
+
.slice(offset, offset + limit);
|
|
38
|
+
},
|
|
39
|
+
async update(id: string, updates: any) {
|
|
40
|
+
const index = storedEntries.findIndex((entry) => entry.id === id);
|
|
41
|
+
if (index < 0) return null;
|
|
42
|
+
storedEntries[index] = {
|
|
43
|
+
...storedEntries[index],
|
|
44
|
+
...updates,
|
|
45
|
+
timestamp: updates.timestamp ?? storedEntries[index].timestamp,
|
|
46
|
+
};
|
|
47
|
+
return storedEntries[index];
|
|
48
|
+
},
|
|
49
|
+
async getById(id: string) {
|
|
50
|
+
return storedEntries.find((entry) => entry.id === id) || null;
|
|
51
|
+
},
|
|
52
|
+
async get(id: string) {
|
|
53
|
+
const exact = storedEntries.find((entry) => entry.id === id);
|
|
54
|
+
if (exact) return exact;
|
|
55
|
+
const matches = storedEntries.filter((entry) => entry.id.startsWith(id));
|
|
56
|
+
if (matches.length > 1) {
|
|
57
|
+
throw new Error(`Ambiguous prefix "${id}" matches ${matches.length} memories. Use a longer prefix or full ID.`);
|
|
58
|
+
}
|
|
59
|
+
return matches[0] || null;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
conflictStore: {
|
|
63
|
+
async save(record: any) {
|
|
64
|
+
conflicts.push(record);
|
|
65
|
+
return record;
|
|
66
|
+
},
|
|
67
|
+
async replace(record: any) {
|
|
68
|
+
const index = conflicts.findIndex((item) => item.conflictId === record.conflictId);
|
|
69
|
+
if (index >= 0) {
|
|
70
|
+
conflicts[index] = record;
|
|
71
|
+
} else {
|
|
72
|
+
conflicts.push(record);
|
|
73
|
+
}
|
|
74
|
+
return record;
|
|
75
|
+
},
|
|
76
|
+
async getOpenByFingerprint(fingerprint: string) {
|
|
77
|
+
return conflicts.find((item) => item.status === "open" && item.fingerprint === fingerprint) || null;
|
|
78
|
+
},
|
|
79
|
+
async getLatestByFingerprint(fingerprint: string) {
|
|
80
|
+
return conflicts.find((item) => item.fingerprint === fingerprint) || null;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildEvidenceEntry(
|
|
88
|
+
deps: ReturnType<typeof createDeps>["deps"],
|
|
89
|
+
text: string,
|
|
90
|
+
scope: string,
|
|
91
|
+
) {
|
|
92
|
+
return deps.store.store({
|
|
93
|
+
text,
|
|
94
|
+
vector: [1, 2, 3],
|
|
95
|
+
category: "events",
|
|
96
|
+
scope,
|
|
97
|
+
importance: 0.55,
|
|
98
|
+
metadata: JSON.stringify({
|
|
99
|
+
source: "cc",
|
|
100
|
+
boundary: {
|
|
101
|
+
layer: "evidence",
|
|
102
|
+
authority: "transcript-ingest",
|
|
103
|
+
conflictPolicy: "append-only",
|
|
104
|
+
originalCategory: "preferences",
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function toResult(entry: any, score = 0.91): RetrievalResult {
|
|
111
|
+
return {
|
|
112
|
+
entry,
|
|
113
|
+
score,
|
|
114
|
+
sources: {
|
|
115
|
+
vector: { score: 0.9, rank: 1 },
|
|
116
|
+
bm25: { score: 0.8, rank: 2 },
|
|
117
|
+
fused: { score },
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe("preference guard smoke", () => {
|
|
123
|
+
it("renders slot provenance end-to-end for a promoted reply-style preference", async () => {
|
|
124
|
+
const { deps, storedEntries, conflicts } = createDeps();
|
|
125
|
+
const source = await buildEvidenceEntry(
|
|
126
|
+
deps,
|
|
127
|
+
"[用户] 用户偏好短句直说。",
|
|
128
|
+
"cc:session-reply-style-smoke",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const promoted = await promoteMemory(deps as any, {
|
|
132
|
+
memoryId: source.id,
|
|
133
|
+
text: "User prefers concise, direct replies.",
|
|
134
|
+
category: "preferences",
|
|
135
|
+
scope: TEST_SCOPE,
|
|
136
|
+
tags: ["writing"],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const entry = storedEntries.find((item) => item.id === promoted.id);
|
|
140
|
+
expect(entry).toBeDefined();
|
|
141
|
+
|
|
142
|
+
const searchOutput = formatSearchResults([toResult(entry)], {
|
|
143
|
+
query: "reply style",
|
|
144
|
+
profile: "default",
|
|
145
|
+
});
|
|
146
|
+
const explainOutput = formatExplainResults([toResult(entry)], {
|
|
147
|
+
query: "reply style",
|
|
148
|
+
profile: "default",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(searchOutput).toContain("key:preferences:reply-style:concise:direct");
|
|
152
|
+
expect(searchOutput).toContain("slot:reply-style:concise:direct");
|
|
153
|
+
expect(searchOutput).toContain(`promoted:${source.id.slice(0, 8)}<-evidence/transcript-ingest`);
|
|
154
|
+
expect(explainOutput).toContain("slot:reply-style:concise:direct");
|
|
155
|
+
expect(explainOutput).toContain(`promoted:${source.id.slice(0, 8)}<-evidence/transcript-ingest`);
|
|
156
|
+
expect(conflicts).toHaveLength(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("keeps descriptive draft and migration notes on plain preferences keys end-to-end", async () => {
|
|
160
|
+
const { deps, storedEntries, conflicts } = createDeps();
|
|
161
|
+
const draftSource = await buildEvidenceEntry(
|
|
162
|
+
deps,
|
|
163
|
+
"[用户] 这段文案简洁直接,先别改。",
|
|
164
|
+
"cc:session-draft-note-smoke",
|
|
165
|
+
);
|
|
166
|
+
const migrationSource = await buildEvidenceEntry(
|
|
167
|
+
deps,
|
|
168
|
+
"[用户] 文档里写了 uses Bun over Node 的迁移说明。",
|
|
169
|
+
"cc:session-migration-note-smoke",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const promotedDraft = await promoteMemory(deps as any, {
|
|
173
|
+
memoryId: draftSource.id,
|
|
174
|
+
text: "这段文案简洁直接,先别改。",
|
|
175
|
+
category: "preferences",
|
|
176
|
+
scope: TEST_SCOPE,
|
|
177
|
+
tags: ["writing"],
|
|
178
|
+
});
|
|
179
|
+
const promotedMigration = await promoteMemory(deps as any, {
|
|
180
|
+
memoryId: migrationSource.id,
|
|
181
|
+
text: "文档里写了 uses Bun over Node 的迁移说明。",
|
|
182
|
+
category: "preferences",
|
|
183
|
+
scope: TEST_SCOPE,
|
|
184
|
+
tags: ["tooling"],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const results = [
|
|
188
|
+
storedEntries.find((item) => item.id === promotedDraft.id),
|
|
189
|
+
storedEntries.find((item) => item.id === promotedMigration.id),
|
|
190
|
+
].filter(Boolean).map((entry) => toResult(entry));
|
|
191
|
+
|
|
192
|
+
const searchOutput = formatSearchResults(results, {
|
|
193
|
+
query: "preferences",
|
|
194
|
+
profile: "default",
|
|
195
|
+
});
|
|
196
|
+
const explainOutput = formatExplainResults(results, {
|
|
197
|
+
query: "preferences",
|
|
198
|
+
profile: "default",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(searchOutput).toContain("key:preferences:这段文案简洁直接-先别改");
|
|
202
|
+
expect(searchOutput).toContain("key:preferences:文档里写了-uses-bun-over-node-的迁移说明");
|
|
203
|
+
expect(searchOutput).not.toContain("slot:reply-style:");
|
|
204
|
+
expect(searchOutput).not.toContain("slot:tool-choice:");
|
|
205
|
+
expect(explainOutput).toContain("key:preferences:这段文案简洁直接-先别改");
|
|
206
|
+
expect(explainOutput).toContain("key:preferences:文档里写了-uses-bun-over-node-的迁移说明");
|
|
207
|
+
expect(explainOutput).not.toContain("slot:reply-style:");
|
|
208
|
+
expect(explainOutput).not.toContain("slot:tool-choice:");
|
|
209
|
+
expect(conflicts).toHaveLength(0);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Tier 3.6: Preference Pattern Quick Matching
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* 1. matchPreference returns "create" when no similar preferences exist
|
|
6
|
+
* 2. matchPreference returns "skip" for near-duplicate preferences (no LLM)
|
|
7
|
+
* 3. matchPreference uses LLM when available for decision
|
|
8
|
+
* 4. matchPreference returns "merge" with merged text from LLM
|
|
9
|
+
* 5. matchPreference handles LLM failure gracefully
|
|
10
|
+
* 6. applyPreferenceMatch handles merge and skip correctly
|
|
11
|
+
* 7. Non-preference categories are not filtered
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it } from "bun:test";
|
|
14
|
+
import {
|
|
15
|
+
matchPreference,
|
|
16
|
+
applyPreferenceMatch,
|
|
17
|
+
DEFAULT_PREFERENCE_MATCHER_CONFIG,
|
|
18
|
+
} from "../preference-matcher.js";
|
|
19
|
+
import type { MemoryEntry, MemorySearchResult } from "../store.js";
|
|
20
|
+
import type { LLMClient, DedupDecision } from "../llm-client.js";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function makeEntry(id: string, text: string, category = "preferences"): MemoryEntry {
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
text,
|
|
30
|
+
vector: [1, 0, 0],
|
|
31
|
+
category,
|
|
32
|
+
scope: "project:test",
|
|
33
|
+
importance: 0.7,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
metadata: "{}",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createMockStore(searchResults: MemorySearchResult[] = []) {
|
|
40
|
+
const updates: Array<{ id: string; text?: string }> = [];
|
|
41
|
+
const data = new Map<string, MemoryEntry>();
|
|
42
|
+
for (const r of searchResults) {
|
|
43
|
+
data.set(r.entry.id, { ...r.entry });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
updates,
|
|
48
|
+
data,
|
|
49
|
+
async vectorSearch(
|
|
50
|
+
_vector: number[],
|
|
51
|
+
_limit: number,
|
|
52
|
+
_minScore: number,
|
|
53
|
+
_scopeFilter?: string[],
|
|
54
|
+
) {
|
|
55
|
+
return searchResults;
|
|
56
|
+
},
|
|
57
|
+
async update(id: string, upd: { text?: string; metadata?: string }, _scope?: string[]) {
|
|
58
|
+
const entry = data.get(id);
|
|
59
|
+
if (!entry) return null;
|
|
60
|
+
if (upd.text) {
|
|
61
|
+
entry.text = upd.text;
|
|
62
|
+
updates.push({ id, text: upd.text });
|
|
63
|
+
}
|
|
64
|
+
return entry;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createMockLLM(dedupResult: DedupDecision, synthResult: string | null = "merged preference text"): LLMClient {
|
|
70
|
+
return {
|
|
71
|
+
async dedupDecision(_newText: string, _existingText: string) {
|
|
72
|
+
return dedupResult;
|
|
73
|
+
},
|
|
74
|
+
async synthesizeFragments(_fragments: string[], _query: string) {
|
|
75
|
+
return synthResult;
|
|
76
|
+
},
|
|
77
|
+
} as any;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// matchPreference tests
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
describe("matchPreference", () => {
|
|
85
|
+
const vector = [1, 0, 0];
|
|
86
|
+
const scope = "project:test";
|
|
87
|
+
|
|
88
|
+
it("returns create when no similar preferences found", async () => {
|
|
89
|
+
const store = createMockStore([]);
|
|
90
|
+
const result = await matchPreference("I prefer dark mode", vector, scope, store as any, null);
|
|
91
|
+
|
|
92
|
+
expect(result.action).toBe("create");
|
|
93
|
+
expect(result.reason).toBe("no-similar-preferences");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns create when matches are non-preference category", async () => {
|
|
97
|
+
const store = createMockStore([
|
|
98
|
+
{ entry: makeEntry("e1", "some entity", "entities"), score: 0.85 },
|
|
99
|
+
]);
|
|
100
|
+
const result = await matchPreference("I prefer dark mode", vector, scope, store as any, null);
|
|
101
|
+
|
|
102
|
+
expect(result.action).toBe("create");
|
|
103
|
+
expect(result.reason).toBe("no-similar-preferences");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns skip for high similarity without LLM", async () => {
|
|
107
|
+
const store = createMockStore([
|
|
108
|
+
{ entry: makeEntry("p1", "User prefers dark mode"), score: 0.95 },
|
|
109
|
+
]);
|
|
110
|
+
const result = await matchPreference("I like dark mode", vector, scope, store as any, null);
|
|
111
|
+
|
|
112
|
+
expect(result.action).toBe("skip");
|
|
113
|
+
expect(result.reason).toContain("no-llm");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("returns create for moderate similarity without LLM", async () => {
|
|
117
|
+
const store = createMockStore([
|
|
118
|
+
{ entry: makeEntry("p1", "User prefers dark mode"), score: 0.82 },
|
|
119
|
+
]);
|
|
120
|
+
const result = await matchPreference("I like vim keybindings", vector, scope, store as any, null);
|
|
121
|
+
|
|
122
|
+
expect(result.action).toBe("create");
|
|
123
|
+
expect(result.reason).toContain("no-llm");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("uses LLM to decide SKIP", async () => {
|
|
127
|
+
const store = createMockStore([
|
|
128
|
+
{ entry: makeEntry("p1", "User prefers dark mode"), score: 0.85 },
|
|
129
|
+
]);
|
|
130
|
+
const llm = createMockLLM({ action: "SKIP", reason: "same preference" });
|
|
131
|
+
|
|
132
|
+
const result = await matchPreference("I like dark mode", vector, scope, store as any, llm);
|
|
133
|
+
|
|
134
|
+
expect(result.action).toBe("skip");
|
|
135
|
+
expect(result.reason).toContain("llm-skip");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("uses LLM to decide MERGE and generates merged text", async () => {
|
|
139
|
+
const store = createMockStore([
|
|
140
|
+
{ entry: makeEntry("p1", "User prefers dark mode"), score: 0.85 },
|
|
141
|
+
]);
|
|
142
|
+
const llm = createMockLLM(
|
|
143
|
+
{ action: "MERGE", reason: "compatible preferences" },
|
|
144
|
+
"User prefers dark mode and compact layout",
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const result = await matchPreference("I also like compact layout", vector, scope, store as any, llm);
|
|
148
|
+
|
|
149
|
+
expect(result.action).toBe("merge");
|
|
150
|
+
expect(result.mergeTargetId).toBe("p1");
|
|
151
|
+
expect(result.mergedText).toBe("User prefers dark mode and compact layout");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("uses LLM to decide CREATE", async () => {
|
|
155
|
+
const store = createMockStore([
|
|
156
|
+
{ entry: makeEntry("p1", "User prefers dark mode"), score: 0.80 },
|
|
157
|
+
]);
|
|
158
|
+
const llm = createMockLLM({ action: "CREATE", reason: "different preference" });
|
|
159
|
+
|
|
160
|
+
const result = await matchPreference("I prefer vim keybindings", vector, scope, store as any, llm);
|
|
161
|
+
|
|
162
|
+
expect(result.action).toBe("create");
|
|
163
|
+
expect(result.reason).toContain("llm-create");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("falls back to create on LLM failure", async () => {
|
|
167
|
+
const store = createMockStore([
|
|
168
|
+
{ entry: makeEntry("p1", "User prefers dark mode"), score: 0.85 },
|
|
169
|
+
]);
|
|
170
|
+
const llm = {
|
|
171
|
+
async dedupDecision() { throw new Error("LLM down"); },
|
|
172
|
+
async synthesizeFragments() { return null; },
|
|
173
|
+
} as any;
|
|
174
|
+
|
|
175
|
+
const result = await matchPreference("test pref", vector, scope, store as any, llm);
|
|
176
|
+
|
|
177
|
+
expect(result.action).toBe("create");
|
|
178
|
+
expect(result.reason).toBe("llm-error");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// applyPreferenceMatch tests
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
describe("applyPreferenceMatch", () => {
|
|
187
|
+
it("handles skip action", async () => {
|
|
188
|
+
const store = createMockStore([]);
|
|
189
|
+
const result = await applyPreferenceMatch(
|
|
190
|
+
{ action: "skip", reason: "duplicate" },
|
|
191
|
+
store as any,
|
|
192
|
+
"project:test",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(result.handled).toBe(true);
|
|
196
|
+
expect(result.entry).toBeUndefined();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("handles merge action — updates existing entry text", async () => {
|
|
200
|
+
const existing = makeEntry("p1", "old preference text");
|
|
201
|
+
const store = createMockStore([{ entry: existing, score: 0.9 }]);
|
|
202
|
+
|
|
203
|
+
const result = await applyPreferenceMatch(
|
|
204
|
+
{
|
|
205
|
+
action: "merge",
|
|
206
|
+
mergeTargetId: "p1",
|
|
207
|
+
mergedText: "merged preference text",
|
|
208
|
+
reason: "llm-merge",
|
|
209
|
+
},
|
|
210
|
+
store as any,
|
|
211
|
+
"project:test",
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(result.handled).toBe(true);
|
|
215
|
+
expect(result.entry).toBeDefined();
|
|
216
|
+
expect(store.updates.length).toBe(1);
|
|
217
|
+
expect(store.updates[0].text).toBe("merged preference text");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("returns not handled for create action", async () => {
|
|
221
|
+
const store = createMockStore([]);
|
|
222
|
+
const result = await applyPreferenceMatch(
|
|
223
|
+
{ action: "create", reason: "new" },
|
|
224
|
+
store as any,
|
|
225
|
+
"project:test",
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(result.handled).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
});
|