recallnest 2.2.0 → 2.3.0
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/.env +4 -0
- package/CLAUDE.md +1 -1
- package/README.md +30 -5
- package/README_CN.md +30 -5
- package/config.json +55 -0
- 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/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +60 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/ingest-2026-04-13.log +87 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +1 -1
- package/src/cli.ts +30 -0
- package/src/connector-types.ts +57 -0
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +31 -1
- package/src/doctor.ts +29 -0
- package/src/ingest.ts +203 -0
- package/src/mcp-server.ts +2 -2
- package/src/obsidian-connector.ts +282 -0
- package/src/session-schema.ts +2 -2
- package/src/source-heartbeat.ts +108 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.dockerignore +0 -11
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/Dockerfile +0 -20
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -245
- package/SKILL.md +0 -250
- package/assets/dashboard.png +0 -0
- package/assets/knowledge-graph.png +0 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/screenshots/ui-full.png +0 -0
- package/assets/screenshots/ui-overview.png +0 -0
- package/assets/screenshots/ui-search.png +0 -0
- package/assets/ui/app.js +0 -1031
- package/assets/ui/index.html +0 -224
- package/assets/ui/styles.css +0 -781
- package/assets/wechat-qr.jpg +0 -0
- package/bun.lock +0 -337
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/daily-reflections/2026-04-02.md +0 -35
- package/daily-reflections/2026-04-03.md +0 -37
- package/daily-reflections/2026-04-04.md +0 -37
- package/daily-reflections/2026-04-05.md +0 -35
- package/daily-reflections/2026-04-06.md +0 -36
- package/daily-reflections/2026-04-07.md +0 -35
- package/daily-reflections/2026-04-08.md +0 -35
- package/daily-reflections/2026-04-12.md +0 -35
- package/data/alias-map.example.json +0 -22
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/2026-04-01-recall-boost.md +0 -100
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +0 -1195
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +0 -398
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/glama.json +0 -6
- package/lm +0 -19
- package/plan-philosophy-upgrades.md +0 -363
- package/plan-product-upgrades.md +0 -155
- package/plan-v1.2-distributable.md +0 -105
- package/plan.md +0 -140
- package/research-memory-evolution.md +0 -440
- package/research-rl-agent-memory.md +0 -330
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-anchors.ts +0 -130
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/diagnose-vector-search.ts +0 -132
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/migrate-language.ts +0 -75
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/verify-emotion-decay.ts +0 -148
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/activity-counter.test.ts +0 -96
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -139
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1144
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -33
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -712
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -249
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -225
- package/src/__tests__/decay-exemption.test.ts +0 -84
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -176
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/dream-pipeline.test.ts +0 -232
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -243
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -271
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -89
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-distiller.test.ts +0 -611
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -546
- package/src/__tests__/skill-promotion.test.ts +0 -349
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -828
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
- package/src/__tests__/write-verifier.test.ts +0 -122
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { scanForPromotions, formatPromotionResult, type PromotionScanResult } from "../skill-promotion.js";
|
|
4
|
-
import type { MemoryEntry, MemorySearchResult } from "../store.js";
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Mock Helpers
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
let idSeq = 0;
|
|
11
|
-
|
|
12
|
-
function makeEntry(overrides: Partial<MemoryEntry> = {}): MemoryEntry {
|
|
13
|
-
idSeq += 1;
|
|
14
|
-
return {
|
|
15
|
-
id: `entry-${String(idSeq).padStart(4, "0")}`,
|
|
16
|
-
text: `Case about deploying to production #${idSeq}`,
|
|
17
|
-
vector: [1, 0, 0],
|
|
18
|
-
category: "cases",
|
|
19
|
-
scope: "project:test",
|
|
20
|
-
importance: 0.7,
|
|
21
|
-
timestamp: 1_700_000_000_000 + idSeq,
|
|
22
|
-
metadata: JSON.stringify({
|
|
23
|
-
evolution: {
|
|
24
|
-
status: "active",
|
|
25
|
-
version: 1,
|
|
26
|
-
accessCount: 0,
|
|
27
|
-
lastAccessedAt: null,
|
|
28
|
-
supersededBy: null,
|
|
29
|
-
consolidatedInto: null,
|
|
30
|
-
sourceMemories: [],
|
|
31
|
-
validFrom: Date.now(),
|
|
32
|
-
validUntil: null,
|
|
33
|
-
},
|
|
34
|
-
}),
|
|
35
|
-
...overrides,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Create a vector that is similar to the base [1,0,0] with controlled similarity. */
|
|
40
|
-
function similarVector(sim: number): number[] {
|
|
41
|
-
// For cosine similarity, we want cos(theta) = sim
|
|
42
|
-
// Use 2D vector [sim, sqrt(1-sim^2), 0] dot [1, 0, 0] = sim
|
|
43
|
-
const complement = Math.sqrt(1 - sim * sim);
|
|
44
|
-
return [sim, complement, 0];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function createMockStore(entries: MemoryEntry[]) {
|
|
48
|
-
return {
|
|
49
|
-
async list(
|
|
50
|
-
_scopeFilter?: string[],
|
|
51
|
-
_category?: string,
|
|
52
|
-
_limit?: number,
|
|
53
|
-
_offset?: number,
|
|
54
|
-
): Promise<MemoryEntry[]> {
|
|
55
|
-
return entries;
|
|
56
|
-
},
|
|
57
|
-
async vectorSearch(
|
|
58
|
-
vector: number[],
|
|
59
|
-
limit = 5,
|
|
60
|
-
minScore = 0.3,
|
|
61
|
-
_scopeFilter?: string[],
|
|
62
|
-
): Promise<MemorySearchResult[]> {
|
|
63
|
-
// Simple mock: return entries sorted by cosine similarity to vector
|
|
64
|
-
const scored = entries
|
|
65
|
-
.filter(e => e.vector?.length > 0)
|
|
66
|
-
.map(e => {
|
|
67
|
-
let dot = 0, normA = 0, normB = 0;
|
|
68
|
-
for (let i = 0; i < vector.length; i++) {
|
|
69
|
-
dot += vector[i] * (e.vector[i] ?? 0);
|
|
70
|
-
normA += vector[i] * vector[i];
|
|
71
|
-
normB += (e.vector[i] ?? 0) * (e.vector[i] ?? 0);
|
|
72
|
-
}
|
|
73
|
-
const score = (normA > 0 && normB > 0)
|
|
74
|
-
? dot / (Math.sqrt(normA) * Math.sqrt(normB))
|
|
75
|
-
: 0;
|
|
76
|
-
return { entry: e, score };
|
|
77
|
-
})
|
|
78
|
-
.filter(r => r.score >= minScore)
|
|
79
|
-
.sort((a, b) => b.score - a.score)
|
|
80
|
-
.slice(0, limit);
|
|
81
|
-
return scored;
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// Tests
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
describe("scanForPromotions", () => {
|
|
91
|
-
it("returns 0 candidates for empty scope", async () => {
|
|
92
|
-
const store = createMockStore([]);
|
|
93
|
-
const result = await scanForPromotions(store, "project:empty");
|
|
94
|
-
|
|
95
|
-
expect(result.candidates).toHaveLength(0);
|
|
96
|
-
expect(result.scannedCases).toBe(0);
|
|
97
|
-
expect(result.scannedPatterns).toBe(0);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("returns 0 candidates when fewer than minCaseOccurrences similar cases", async () => {
|
|
101
|
-
// Two cases with the same vector — below default threshold of 3
|
|
102
|
-
const entries = [
|
|
103
|
-
makeEntry({ vector: [1, 0, 0] }),
|
|
104
|
-
makeEntry({ vector: [0.99, 0.14, 0] }),
|
|
105
|
-
];
|
|
106
|
-
const store = createMockStore(entries);
|
|
107
|
-
const result = await scanForPromotions(store, "project:test");
|
|
108
|
-
|
|
109
|
-
expect(result.candidates).toHaveLength(0);
|
|
110
|
-
expect(result.scannedCases).toBe(2);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("produces case_to_pattern candidate when 3+ similar cases exist", async () => {
|
|
114
|
-
const entries = [
|
|
115
|
-
makeEntry({ text: "Deploy error: missing env var", vector: [1, 0, 0] }),
|
|
116
|
-
makeEntry({ text: "Deploy failure: env variable not set", vector: similarVector(0.95) }),
|
|
117
|
-
makeEntry({ text: "Deploy issue: environment config missing", vector: similarVector(0.90) }),
|
|
118
|
-
];
|
|
119
|
-
const store = createMockStore(entries);
|
|
120
|
-
const result = await scanForPromotions(store, "project:test", {
|
|
121
|
-
minCaseOccurrences: 3,
|
|
122
|
-
caseSimilarityThreshold: 0.75,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
expect(result.candidates.length).toBeGreaterThanOrEqual(1);
|
|
126
|
-
const caseCandidate = result.candidates.find(c => c.type === "case_to_pattern");
|
|
127
|
-
expect(caseCandidate).toBeDefined();
|
|
128
|
-
expect(caseCandidate!.sourceEntries.length).toBeGreaterThanOrEqual(3);
|
|
129
|
-
expect(caseCandidate!.suggestedName).toBeTruthy();
|
|
130
|
-
expect(caseCandidate!.suggestedDescription).toContain("cases");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("does not produce candidates from dissimilar cases", async () => {
|
|
134
|
-
// 3 cases with very different vectors
|
|
135
|
-
const entries = [
|
|
136
|
-
makeEntry({ text: "Case A", vector: [1, 0, 0] }),
|
|
137
|
-
makeEntry({ text: "Case B", vector: [0, 1, 0] }),
|
|
138
|
-
makeEntry({ text: "Case C", vector: [0, 0, 1] }),
|
|
139
|
-
];
|
|
140
|
-
const store = createMockStore(entries);
|
|
141
|
-
const result = await scanForPromotions(store, "project:test", {
|
|
142
|
-
minCaseOccurrences: 3,
|
|
143
|
-
caseSimilarityThreshold: 0.75,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
expect(result.candidates).toHaveLength(0);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("computes confidence using Bayesian smoothing", async () => {
|
|
150
|
-
// 4 similar cases: confidence = 4 / (4 + 2) = 0.667
|
|
151
|
-
const entries = [
|
|
152
|
-
makeEntry({ text: "Deploy error A", vector: [1, 0, 0] }),
|
|
153
|
-
makeEntry({ text: "Deploy error B", vector: similarVector(0.95) }),
|
|
154
|
-
makeEntry({ text: "Deploy error C", vector: similarVector(0.92) }),
|
|
155
|
-
makeEntry({ text: "Deploy error D", vector: similarVector(0.88) }),
|
|
156
|
-
];
|
|
157
|
-
const store = createMockStore(entries);
|
|
158
|
-
const result = await scanForPromotions(store, "project:test", {
|
|
159
|
-
minCaseOccurrences: 3,
|
|
160
|
-
caseSimilarityThreshold: 0.75,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
expect(result.candidates.length).toBeGreaterThanOrEqual(1);
|
|
164
|
-
const c = result.candidates[0];
|
|
165
|
-
// cluster of 4: confidence = 4/(4+2) = 0.6667
|
|
166
|
-
expect(c.confidence).toBeCloseTo(4 / 6, 2);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("produces pattern_to_skill when pattern has steps and related cases", async () => {
|
|
170
|
-
const patternVec: number[] = [0.9, 0.43, 0]; // cos sim with [1,0,0] ~ 0.9
|
|
171
|
-
const entries = [
|
|
172
|
-
makeEntry({
|
|
173
|
-
category: "patterns",
|
|
174
|
-
text: "Production Deploy Pattern\n\nSteps:\n1. Check environment\n2. Run preflight\n3. Deploy containers",
|
|
175
|
-
vector: patternVec,
|
|
176
|
-
}),
|
|
177
|
-
makeEntry({ text: "Deploy case: ran preflight then deployed", vector: [1, 0, 0] }),
|
|
178
|
-
makeEntry({ text: "Deploy fix: environment check before push", vector: similarVector(0.88) }),
|
|
179
|
-
];
|
|
180
|
-
const store = createMockStore(entries);
|
|
181
|
-
const result = await scanForPromotions(store, "project:test", {
|
|
182
|
-
caseSimilarityThreshold: 0.75,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const skillCandidate = result.candidates.find(c => c.type === "pattern_to_skill");
|
|
186
|
-
expect(skillCandidate).toBeDefined();
|
|
187
|
-
expect(skillCandidate!.suggestedImplementation).toContain("Check environment");
|
|
188
|
-
expect(skillCandidate!.sourceEntries.length).toBeGreaterThanOrEqual(3); // pattern + 2 cases
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("does not produce pattern_to_skill when pattern lacks structured steps", async () => {
|
|
192
|
-
const patternVec: number[] = [0.9, 0.43, 0];
|
|
193
|
-
const entries = [
|
|
194
|
-
makeEntry({
|
|
195
|
-
category: "patterns",
|
|
196
|
-
text: "Just a general note about deploying without structured steps",
|
|
197
|
-
vector: patternVec,
|
|
198
|
-
}),
|
|
199
|
-
makeEntry({ text: "Deploy case one", vector: [1, 0, 0] }),
|
|
200
|
-
makeEntry({ text: "Deploy case two", vector: similarVector(0.88) }),
|
|
201
|
-
];
|
|
202
|
-
const store = createMockStore(entries);
|
|
203
|
-
const result = await scanForPromotions(store, "project:test", {
|
|
204
|
-
caseSimilarityThreshold: 0.75,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const skillCandidate = result.candidates.find(c => c.type === "pattern_to_skill");
|
|
208
|
-
expect(skillCandidate).toBeUndefined();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("respects maxCandidates limit", async () => {
|
|
212
|
-
// Create two distinct clusters of 3, but set maxCandidates=1
|
|
213
|
-
const entries = [
|
|
214
|
-
// Cluster 1
|
|
215
|
-
makeEntry({ text: "Deploy A", vector: [1, 0, 0] }),
|
|
216
|
-
makeEntry({ text: "Deploy B", vector: similarVector(0.95) }),
|
|
217
|
-
makeEntry({ text: "Deploy C", vector: similarVector(0.90) }),
|
|
218
|
-
// Cluster 2
|
|
219
|
-
makeEntry({ text: "Auth fix A", vector: [0, 1, 0] }),
|
|
220
|
-
makeEntry({ text: "Auth fix B", vector: [0.1, 0.99, 0] }),
|
|
221
|
-
makeEntry({ text: "Auth fix C", vector: [0.15, 0.98, 0] }),
|
|
222
|
-
];
|
|
223
|
-
const store = createMockStore(entries);
|
|
224
|
-
const result = await scanForPromotions(store, "project:test", {
|
|
225
|
-
minCaseOccurrences: 3,
|
|
226
|
-
caseSimilarityThreshold: 0.75,
|
|
227
|
-
maxCandidates: 1,
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
expect(result.candidates).toHaveLength(1);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("filters out archived entries", async () => {
|
|
234
|
-
const archivedMeta = JSON.stringify({
|
|
235
|
-
evolution: {
|
|
236
|
-
status: "archived",
|
|
237
|
-
version: 1,
|
|
238
|
-
accessCount: 0,
|
|
239
|
-
lastAccessedAt: null,
|
|
240
|
-
supersededBy: null,
|
|
241
|
-
consolidatedInto: null,
|
|
242
|
-
sourceMemories: [],
|
|
243
|
-
validFrom: Date.now(),
|
|
244
|
-
validUntil: null,
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
const entries = [
|
|
248
|
-
makeEntry({ text: "Active case", vector: [1, 0, 0] }),
|
|
249
|
-
makeEntry({ text: "Archived case", vector: similarVector(0.95), metadata: archivedMeta }),
|
|
250
|
-
makeEntry({ text: "Another archived", vector: similarVector(0.90), metadata: archivedMeta }),
|
|
251
|
-
];
|
|
252
|
-
const store = createMockStore(entries);
|
|
253
|
-
const result = await scanForPromotions(store, "project:test", {
|
|
254
|
-
minCaseOccurrences: 3,
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// Only 1 active case, so no candidates
|
|
258
|
-
expect(result.candidates).toHaveLength(0);
|
|
259
|
-
expect(result.scannedCases).toBe(1);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("skips entries without vectors", async () => {
|
|
263
|
-
const entries = [
|
|
264
|
-
makeEntry({ text: "No vector case A", vector: [] }),
|
|
265
|
-
makeEntry({ text: "No vector case B", vector: [] }),
|
|
266
|
-
makeEntry({ text: "No vector case C", vector: [] }),
|
|
267
|
-
];
|
|
268
|
-
const store = createMockStore(entries);
|
|
269
|
-
const result = await scanForPromotions(store, "project:test");
|
|
270
|
-
|
|
271
|
-
expect(result.candidates).toHaveLength(0);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("candidates are sorted by confidence descending", async () => {
|
|
275
|
-
// Cluster 1: 5 similar cases -> confidence 5/7
|
|
276
|
-
// Cluster 2: 3 similar cases -> confidence 3/5
|
|
277
|
-
const entries = [
|
|
278
|
-
// Large cluster
|
|
279
|
-
makeEntry({ text: "Deploy err 1", vector: [1, 0, 0] }),
|
|
280
|
-
makeEntry({ text: "Deploy err 2", vector: similarVector(0.95) }),
|
|
281
|
-
makeEntry({ text: "Deploy err 3", vector: similarVector(0.93) }),
|
|
282
|
-
makeEntry({ text: "Deploy err 4", vector: similarVector(0.91) }),
|
|
283
|
-
makeEntry({ text: "Deploy err 5", vector: similarVector(0.89) }),
|
|
284
|
-
// Small cluster (orthogonal direction)
|
|
285
|
-
makeEntry({ text: "Auth bug 1", vector: [0, 1, 0] }),
|
|
286
|
-
makeEntry({ text: "Auth bug 2", vector: [0.1, 0.99, 0] }),
|
|
287
|
-
makeEntry({ text: "Auth bug 3", vector: [0.15, 0.98, 0] }),
|
|
288
|
-
];
|
|
289
|
-
const store = createMockStore(entries);
|
|
290
|
-
const result = await scanForPromotions(store, "project:test", {
|
|
291
|
-
minCaseOccurrences: 3,
|
|
292
|
-
caseSimilarityThreshold: 0.75,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
if (result.candidates.length >= 2) {
|
|
296
|
-
expect(result.candidates[0].confidence).toBeGreaterThanOrEqual(result.candidates[1].confidence);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
describe("formatPromotionResult", () => {
|
|
302
|
-
it("formats empty results", () => {
|
|
303
|
-
const result: PromotionScanResult = {
|
|
304
|
-
candidates: [],
|
|
305
|
-
scannedCases: 5,
|
|
306
|
-
scannedPatterns: 2,
|
|
307
|
-
};
|
|
308
|
-
const text = formatPromotionResult(result);
|
|
309
|
-
expect(text).toContain("5 cases");
|
|
310
|
-
expect(text).toContain("2 patterns");
|
|
311
|
-
expect(text).toContain("No promotion candidates found");
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("formats candidates with type and confidence", () => {
|
|
315
|
-
const result: PromotionScanResult = {
|
|
316
|
-
candidates: [{
|
|
317
|
-
type: "case_to_pattern",
|
|
318
|
-
sourceEntries: [{ id: "a", text: "test", score: 0.9 }],
|
|
319
|
-
suggestedName: "Test Pattern",
|
|
320
|
-
suggestedDescription: "A test",
|
|
321
|
-
confidence: 0.6,
|
|
322
|
-
}],
|
|
323
|
-
scannedCases: 3,
|
|
324
|
-
scannedPatterns: 1,
|
|
325
|
-
};
|
|
326
|
-
const text = formatPromotionResult(result);
|
|
327
|
-
expect(text).toContain("case_to_pattern");
|
|
328
|
-
expect(text).toContain("Test Pattern");
|
|
329
|
-
expect(text).toContain("60.0%");
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("includes implementation in pattern_to_skill output", () => {
|
|
333
|
-
const result: PromotionScanResult = {
|
|
334
|
-
candidates: [{
|
|
335
|
-
type: "pattern_to_skill",
|
|
336
|
-
sourceEntries: [{ id: "a", text: "test", score: 1.0 }],
|
|
337
|
-
suggestedName: "Deploy Skill",
|
|
338
|
-
suggestedDescription: "Deploy automation",
|
|
339
|
-
suggestedImplementation: "1. Check env\n2. Deploy",
|
|
340
|
-
confidence: 0.5,
|
|
341
|
-
}],
|
|
342
|
-
scannedCases: 2,
|
|
343
|
-
scannedPatterns: 1,
|
|
344
|
-
};
|
|
345
|
-
const text = formatPromotionResult(result);
|
|
346
|
-
expect(text).toContain("1. Check env");
|
|
347
|
-
expect(text).toContain("Implementation:");
|
|
348
|
-
});
|
|
349
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { MemoryStore, classifyLegacyScope } from "../store.js";
|
|
7
|
-
|
|
8
|
-
const cleanupPaths: string[] = [];
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
while (cleanupPaths.length > 0) {
|
|
12
|
-
const target = cleanupPaths.pop();
|
|
13
|
-
if (target) {
|
|
14
|
-
rmSync(target, { recursive: true, force: true });
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
function createStore(): MemoryStore {
|
|
20
|
-
const dbPath = mkdtempSync(join(tmpdir(), "recallnest-scope-audit-"));
|
|
21
|
-
cleanupPaths.push(dbPath);
|
|
22
|
-
return new MemoryStore({
|
|
23
|
-
dbPath,
|
|
24
|
-
vectorDim: 3,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("classifyLegacyScope", () => {
|
|
29
|
-
it("flags missing, empty, and global scope values", () => {
|
|
30
|
-
expect(classifyLegacyScope(undefined)).toBe("missing");
|
|
31
|
-
expect(classifyLegacyScope(null)).toBe("missing");
|
|
32
|
-
expect(classifyLegacyScope("")).toBe("empty");
|
|
33
|
-
expect(classifyLegacyScope(" ")).toBe("empty");
|
|
34
|
-
expect(classifyLegacyScope("global")).toBe("global");
|
|
35
|
-
expect(classifyLegacyScope(" global ")).toBe("global");
|
|
36
|
-
expect(classifyLegacyScope("project:recallnest")).toBeUndefined();
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("MemoryStore.auditLegacyScopes", () => {
|
|
41
|
-
it("reports global and empty scope rows without mixing in valid scoped data", async () => {
|
|
42
|
-
const store = createStore();
|
|
43
|
-
|
|
44
|
-
await store.store({
|
|
45
|
-
text: "Scoped memory should not appear in the audit.",
|
|
46
|
-
vector: [1, 0, 0],
|
|
47
|
-
category: "entities",
|
|
48
|
-
scope: "project:recallnest",
|
|
49
|
-
importance: 0.8,
|
|
50
|
-
metadata: "{}",
|
|
51
|
-
});
|
|
52
|
-
await store.store({
|
|
53
|
-
text: "Historical row that used the global fallback.",
|
|
54
|
-
vector: [0, 1, 0],
|
|
55
|
-
category: "events",
|
|
56
|
-
scope: "global",
|
|
57
|
-
importance: 0.5,
|
|
58
|
-
metadata: "{}",
|
|
59
|
-
});
|
|
60
|
-
await store.store({
|
|
61
|
-
text: "Historical row that was written without a real scope.",
|
|
62
|
-
vector: [0, 0, 1],
|
|
63
|
-
category: "events",
|
|
64
|
-
scope: " ",
|
|
65
|
-
importance: 0.4,
|
|
66
|
-
metadata: "{}",
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const audit = await store.auditLegacyScopes(10);
|
|
70
|
-
|
|
71
|
-
expect(audit.totalCount).toBe(2);
|
|
72
|
-
expect(audit.counts).toEqual({
|
|
73
|
-
missing: 0,
|
|
74
|
-
empty: 1,
|
|
75
|
-
global: 1,
|
|
76
|
-
});
|
|
77
|
-
expect(audit.samples).toHaveLength(2);
|
|
78
|
-
expect(audit.samples.map((sample) => sample.kind).sort()).toEqual(["empty", "global"]);
|
|
79
|
-
expect(audit.samples.every((sample) => sample.scope !== "project:recallnest")).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
buildSupersedeMetadata,
|
|
4
|
-
buildSupersedeMetadataForNew,
|
|
5
|
-
parseEvolution,
|
|
6
|
-
traceEvolution,
|
|
7
|
-
patchEvolution,
|
|
8
|
-
defaultEvolution,
|
|
9
|
-
} from "../memory-evolution.js";
|
|
10
|
-
|
|
11
|
-
describe("HP-1: Superseded chain enhancement", () => {
|
|
12
|
-
describe("bidirectional linking", () => {
|
|
13
|
-
it("buildSupersedeMetadata marks old → supersededBy", () => {
|
|
14
|
-
const oldMeta = patchEvolution("{}", defaultEvolution());
|
|
15
|
-
const result = buildSupersedeMetadata(oldMeta, "new-id");
|
|
16
|
-
const evo = parseEvolution(result);
|
|
17
|
-
expect(evo.status).toBe("superseded");
|
|
18
|
-
expect(evo.supersededBy).toBe("new-id");
|
|
19
|
-
expect(evo.validUntil).toBeGreaterThan(0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("buildSupersedeMetadataForNew marks new → supersedes + note", () => {
|
|
23
|
-
const newMeta = patchEvolution("{}", defaultEvolution());
|
|
24
|
-
const result = buildSupersedeMetadataForNew(newMeta, "old-id", "updated preference");
|
|
25
|
-
const evo = parseEvolution(result);
|
|
26
|
-
expect(evo.status).toBe("active"); // new memory stays active
|
|
27
|
-
expect(evo.supersedes).toBe("old-id");
|
|
28
|
-
expect(evo.evolutionNote).toBe("updated preference");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("bidirectional: old.supersededBy == new.id AND new.supersedes == old.id", () => {
|
|
32
|
-
const oldMeta = patchEvolution("{}", defaultEvolution());
|
|
33
|
-
const newMeta = patchEvolution("{}", defaultEvolution());
|
|
34
|
-
|
|
35
|
-
const updatedOld = buildSupersedeMetadata(oldMeta, "new-id");
|
|
36
|
-
const updatedNew = buildSupersedeMetadataForNew(newMeta, "old-id");
|
|
37
|
-
|
|
38
|
-
const oldEvo = parseEvolution(updatedOld);
|
|
39
|
-
const newEvo = parseEvolution(updatedNew);
|
|
40
|
-
|
|
41
|
-
expect(oldEvo.supersededBy).toBe("new-id");
|
|
42
|
-
expect(newEvo.supersedes).toBe("old-id");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("traceEvolution", () => {
|
|
47
|
-
it("traces a 3-node chain", async () => {
|
|
48
|
-
// v1 → v2 → v3
|
|
49
|
-
const entries: Record<string, { metadata: string; timestamp: number }> = {
|
|
50
|
-
v1: {
|
|
51
|
-
metadata: patchEvolution("{}", {
|
|
52
|
-
status: "superseded",
|
|
53
|
-
supersededBy: "v2",
|
|
54
|
-
validFrom: 1000,
|
|
55
|
-
validUntil: 2000,
|
|
56
|
-
}),
|
|
57
|
-
timestamp: 1000,
|
|
58
|
-
},
|
|
59
|
-
v2: {
|
|
60
|
-
metadata: patchEvolution("{}", {
|
|
61
|
-
status: "superseded",
|
|
62
|
-
supersedes: "v1",
|
|
63
|
-
supersededBy: "v3",
|
|
64
|
-
evolutionNote: "added detail",
|
|
65
|
-
validFrom: 2000,
|
|
66
|
-
validUntil: 3000,
|
|
67
|
-
}),
|
|
68
|
-
timestamp: 2000,
|
|
69
|
-
},
|
|
70
|
-
v3: {
|
|
71
|
-
metadata: patchEvolution("{}", {
|
|
72
|
-
status: "active",
|
|
73
|
-
supersedes: "v2",
|
|
74
|
-
evolutionNote: "corrected error",
|
|
75
|
-
validFrom: 3000,
|
|
76
|
-
}),
|
|
77
|
-
timestamp: 3000,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const getEntry = async (id: string) => entries[id] ?? null;
|
|
82
|
-
|
|
83
|
-
// Trace from v2 (middle)
|
|
84
|
-
const trace = await traceEvolution("v2", getEntry);
|
|
85
|
-
expect(trace.length).toBe(3);
|
|
86
|
-
expect(trace[0].id).toBe("v1");
|
|
87
|
-
expect(trace[0].direction).toBe("predecessor");
|
|
88
|
-
expect(trace[1].id).toBe("v2");
|
|
89
|
-
expect(trace[1].direction).toBe("self");
|
|
90
|
-
expect(trace[2].id).toBe("v3");
|
|
91
|
-
expect(trace[2].direction).toBe("successor");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("handles single node (no chain)", async () => {
|
|
95
|
-
const getEntry = async (id: string) => {
|
|
96
|
-
if (id === "solo") return { metadata: patchEvolution("{}", defaultEvolution()), timestamp: 1000 };
|
|
97
|
-
return null;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const trace = await traceEvolution("solo", getEntry);
|
|
101
|
-
expect(trace.length).toBe(1);
|
|
102
|
-
expect(trace[0].direction).toBe("self");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("respects maxDepth", async () => {
|
|
106
|
-
// Create a long chain: v0 → v1 → v2 → ... → v20
|
|
107
|
-
const entries: Record<string, { metadata: string; timestamp: number }> = {};
|
|
108
|
-
for (let i = 0; i <= 20; i++) {
|
|
109
|
-
entries[`v${i}`] = {
|
|
110
|
-
metadata: patchEvolution("{}", {
|
|
111
|
-
status: i < 20 ? "superseded" : "active",
|
|
112
|
-
supersedes: i > 0 ? `v${i - 1}` : null,
|
|
113
|
-
supersededBy: i < 20 ? `v${i + 1}` : null,
|
|
114
|
-
validFrom: i * 1000,
|
|
115
|
-
}),
|
|
116
|
-
timestamp: i * 1000,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const getEntry = async (id: string) => entries[id] ?? null;
|
|
121
|
-
const trace = await traceEvolution("v10", getEntry, 3);
|
|
122
|
-
// maxDepth=3: at most 3 predecessors + self + 3 successors = 7
|
|
123
|
-
expect(trace.length).toBeLessThanOrEqual(7);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("handles missing entries gracefully", async () => {
|
|
127
|
-
const entries: Record<string, { metadata: string; timestamp: number }> = {
|
|
128
|
-
v1: {
|
|
129
|
-
metadata: patchEvolution("{}", {
|
|
130
|
-
status: "active",
|
|
131
|
-
supersedes: "missing",
|
|
132
|
-
validFrom: 1000,
|
|
133
|
-
}),
|
|
134
|
-
timestamp: 1000,
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
const getEntry = async (id: string) => entries[id] ?? null;
|
|
138
|
-
const trace = await traceEvolution("v1", getEntry);
|
|
139
|
-
expect(trace.length).toBe(1); // only self, missing predecessor stops chain
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe("backward compatibility", () => {
|
|
144
|
-
it("parseEvolution defaults new fields to null", () => {
|
|
145
|
-
// Old metadata without supersedes/evolutionNote
|
|
146
|
-
const oldMeta = JSON.stringify({
|
|
147
|
-
evolution: { status: "active", version: 1, accessCount: 0 },
|
|
148
|
-
});
|
|
149
|
-
const evo = parseEvolution(oldMeta);
|
|
150
|
-
expect(evo.supersedes).toBeNull();
|
|
151
|
-
expect(evo.evolutionNote).toBeNull();
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
});
|