recallnest 3.0.0 → 3.0.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/CHANGELOG.md +101 -0
- package/README.md +222 -254
- package/README_CN.md +206 -243
- package/package.json +3 -2
- package/src/capture-engine.ts +26 -4
- package/src/context-composer-stable.ts +5 -1
- package/src/embedder.ts +24 -0
- package/src/env-config.ts +18 -0
- package/src/ingest.ts +225 -17
- package/src/kg-extractor.ts +17 -0
- package/src/memory-output.ts +35 -0
- package/src/pivot-apply.ts +52 -1
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/runtime-config.ts +9 -0
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +9 -1
- package/src/session-store.ts +2 -3
- package/src/skill-promotion.ts +67 -1
- package/src/__tests__/access-tracker-novelty.test.ts +0 -324
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/age-label.test.ts +0 -33
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- 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 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- 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 -116
- 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 -1236
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- 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 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -520
- 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__/contracts.test.ts +0 -262
- 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 -304
- package/src/__tests__/decay-exemption.test.ts +0 -129
- 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 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -820
- package/src/__tests__/dream-scope-isolation.test.ts +0 -386
- package/src/__tests__/dream-write-batching.test.ts +0 -211
- 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__/env-config-invariant.test.ts +0 -80
- package/src/__tests__/env-config.test.ts +0 -211
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- 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__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
- 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 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -197
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
- 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 -421
- package/src/__tests__/memory-output.test.ts +0 -224
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/memory-utility.test.ts +0 -256
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/openai-contract.test.ts +0 -513
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- 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 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.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-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- 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__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -254
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -235
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/synthesis-contract.test.ts +0 -252
- package/src/__tests__/synthesis-promotion.test.ts +0 -369
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -867
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/utility-weight.test.ts +0 -57
- package/src/__tests__/verbatim-self-recall.test.ts +0 -279
- package/src/__tests__/version-manager.test.ts +0 -248
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, beforeAll, afterAll } from "bun:test";
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
isObsidianVault,
|
|
8
|
-
parseFrontmatter,
|
|
9
|
-
extractWikiLinks,
|
|
10
|
-
contentHash,
|
|
11
|
-
scanVault,
|
|
12
|
-
} from "../obsidian-connector.js";
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Temp vault fixture
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
const VAULT_ROOT = join(tmpdir(), `recallnest-obsidian-test-${Date.now()}`);
|
|
19
|
-
|
|
20
|
-
beforeAll(() => {
|
|
21
|
-
// Create vault structure
|
|
22
|
-
mkdirSync(join(VAULT_ROOT, ".obsidian"), { recursive: true });
|
|
23
|
-
mkdirSync(join(VAULT_ROOT, "projects"), { recursive: true });
|
|
24
|
-
mkdirSync(join(VAULT_ROOT, "daily"), { recursive: true });
|
|
25
|
-
mkdirSync(join(VAULT_ROOT, ".trash"), { recursive: true });
|
|
26
|
-
|
|
27
|
-
// Root-level note with frontmatter + wikilinks
|
|
28
|
-
writeFileSync(
|
|
29
|
-
join(VAULT_ROOT, "My Note.md"),
|
|
30
|
-
`---
|
|
31
|
-
tags: [ai, memory]
|
|
32
|
-
aliases: [note-1]
|
|
33
|
-
status: active
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
# My Note
|
|
37
|
-
|
|
38
|
-
This is a note about [[RecallNest]] and [[LanceDB|vector database]].
|
|
39
|
-
|
|
40
|
-
Some more content here to make it long enough.
|
|
41
|
-
`,
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// Nested note in projects/
|
|
45
|
-
writeFileSync(
|
|
46
|
-
join(VAULT_ROOT, "projects", "Project Alpha.md"),
|
|
47
|
-
`---
|
|
48
|
-
tags:
|
|
49
|
-
- project
|
|
50
|
-
- alpha
|
|
51
|
-
priority: high
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
# Project Alpha
|
|
55
|
-
|
|
56
|
-
Working on [[Feature X]] for [[Project Alpha]].
|
|
57
|
-
|
|
58
|
-
Details about the project implementation and design decisions.
|
|
59
|
-
`,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// Daily note without frontmatter
|
|
63
|
-
writeFileSync(
|
|
64
|
-
join(VAULT_ROOT, "daily", "2026-04-13.md"),
|
|
65
|
-
`# 2026-04-13
|
|
66
|
-
|
|
67
|
-
- Met with team about [[Q2 Goals]]
|
|
68
|
-
- Reviewed [[PR #123]]
|
|
69
|
-
- Need to follow up on [[Budget Review]]
|
|
70
|
-
|
|
71
|
-
Some additional notes about the day and what happened.
|
|
72
|
-
`,
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
// Short note (should be skipped)
|
|
76
|
-
writeFileSync(join(VAULT_ROOT, "short.md"), "Hi");
|
|
77
|
-
|
|
78
|
-
// Trash note (should be skipped)
|
|
79
|
-
writeFileSync(
|
|
80
|
-
join(VAULT_ROOT, ".trash", "deleted.md"),
|
|
81
|
-
"This is deleted content that should not be ingested into the vault.\n",
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
afterAll(() => {
|
|
86
|
-
try {
|
|
87
|
-
rmSync(VAULT_ROOT, { recursive: true, force: true });
|
|
88
|
-
} catch {
|
|
89
|
-
// cleanup best-effort
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// isObsidianVault
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
describe("isObsidianVault", () => {
|
|
98
|
-
it("returns true for a directory with .obsidian/", () => {
|
|
99
|
-
expect(isObsidianVault(VAULT_ROOT)).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("returns false for a regular directory", () => {
|
|
103
|
-
expect(isObsidianVault(join(VAULT_ROOT, "projects"))).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("returns false for non-existent directory", () => {
|
|
107
|
-
expect(isObsidianVault("/nonexistent/path")).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// parseFrontmatter
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
describe("parseFrontmatter", () => {
|
|
116
|
-
it("parses inline list tags", () => {
|
|
117
|
-
const content = `---
|
|
118
|
-
tags: [ai, memory]
|
|
119
|
-
status: active
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
Body content here.`;
|
|
123
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
124
|
-
expect(frontmatter).not.toBeNull();
|
|
125
|
-
expect(frontmatter!.tags).toEqual(["ai", "memory"]);
|
|
126
|
-
expect(frontmatter!.status).toBe("active");
|
|
127
|
-
expect(body).toBe("Body content here.");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("parses YAML list tags", () => {
|
|
131
|
-
const content = `---
|
|
132
|
-
tags:
|
|
133
|
-
- project
|
|
134
|
-
- alpha
|
|
135
|
-
priority: high
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
Body here.`;
|
|
139
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
140
|
-
expect(frontmatter).not.toBeNull();
|
|
141
|
-
expect(frontmatter!.tags).toEqual(["project", "alpha"]);
|
|
142
|
-
expect(frontmatter!.priority).toBe("high");
|
|
143
|
-
expect(body).toBe("Body here.");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("returns null frontmatter when no --- block", () => {
|
|
147
|
-
const content = "# Just a heading\n\nSome content.";
|
|
148
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
149
|
-
expect(frontmatter).toBeNull();
|
|
150
|
-
expect(body).toBe(content);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("handles empty frontmatter", () => {
|
|
154
|
-
const content = `---
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
Body.`;
|
|
158
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
159
|
-
expect(frontmatter).toBeNull();
|
|
160
|
-
expect(body).toBe("Body.");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("handles quoted values", () => {
|
|
164
|
-
const content = `---
|
|
165
|
-
title: 'My Title'
|
|
166
|
-
author: "Alice"
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
Content.`;
|
|
170
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
171
|
-
expect(frontmatter).not.toBeNull();
|
|
172
|
-
expect(frontmatter!.title).toBe("My Title");
|
|
173
|
-
expect(frontmatter!.author).toBe("Alice");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("handles empty inline list []", () => {
|
|
177
|
-
const content = `---
|
|
178
|
-
tags: []
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
Content here is long enough.`;
|
|
182
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
183
|
-
// Empty list should not produce tags
|
|
184
|
-
expect(frontmatter).toBeNull();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("handles Windows-style line endings", () => {
|
|
188
|
-
const content = "---\r\ntags: [a, b]\r\n---\r\n\r\nBody.";
|
|
189
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
190
|
-
expect(frontmatter).not.toBeNull();
|
|
191
|
-
expect(frontmatter!.tags).toEqual(["a", "b"]);
|
|
192
|
-
expect(body).toBe("Body.");
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
|
-
// extractWikiLinks
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
|
|
200
|
-
describe("extractWikiLinks", () => {
|
|
201
|
-
it("extracts simple wikilinks", () => {
|
|
202
|
-
const links = extractWikiLinks("Link to [[RecallNest]] and [[LanceDB]].");
|
|
203
|
-
expect(links).toHaveLength(2);
|
|
204
|
-
expect(links[0].target).toBe("RecallNest");
|
|
205
|
-
expect(links[1].target).toBe("LanceDB");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("extracts wikilinks with display text", () => {
|
|
209
|
-
const links = extractWikiLinks("See [[LanceDB|vector database]] for details.");
|
|
210
|
-
expect(links).toHaveLength(1);
|
|
211
|
-
expect(links[0].target).toBe("LanceDB");
|
|
212
|
-
expect(links[0].displayText).toBe("vector database");
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("skips embed syntax ![[embed]]", () => {
|
|
216
|
-
const links = extractWikiLinks("Normal [[link]] and embed ![[image.png]].");
|
|
217
|
-
expect(links).toHaveLength(1);
|
|
218
|
-
expect(links[0].target).toBe("link");
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("handles multiple links on same line", () => {
|
|
222
|
-
const links = extractWikiLinks("[[A]] connects to [[B]] and [[C]]");
|
|
223
|
-
expect(links).toHaveLength(3);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("returns empty array for no links", () => {
|
|
227
|
-
const links = extractWikiLinks("Just plain text.");
|
|
228
|
-
expect(links).toHaveLength(0);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("trims whitespace from targets", () => {
|
|
232
|
-
const links = extractWikiLinks("[[ Spaced Target ]]");
|
|
233
|
-
expect(links[0].target).toBe("Spaced Target");
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// ---------------------------------------------------------------------------
|
|
238
|
-
// contentHash
|
|
239
|
-
// ---------------------------------------------------------------------------
|
|
240
|
-
|
|
241
|
-
describe("contentHash", () => {
|
|
242
|
-
it("returns a 16-char hex string", () => {
|
|
243
|
-
const hash = contentHash("hello world");
|
|
244
|
-
expect(hash).toHaveLength(16);
|
|
245
|
-
expect(/^[0-9a-f]{16}$/.test(hash)).toBe(true);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("is deterministic", () => {
|
|
249
|
-
expect(contentHash("same input")).toBe(contentHash("same input"));
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("differs for different inputs", () => {
|
|
253
|
-
expect(contentHash("input A")).not.toBe(contentHash("input B"));
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// scanVault
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
describe("scanVault", () => {
|
|
262
|
-
it("produces valid connector-v1 output", () => {
|
|
263
|
-
const output = scanVault(VAULT_ROOT);
|
|
264
|
-
expect(output.version).toBe("connector-v1");
|
|
265
|
-
expect(output.source).toBe("obsidian");
|
|
266
|
-
expect(output.scope).toContain("vault:");
|
|
267
|
-
expect(Array.isArray(output.records)).toBe(true);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it("finds all non-short, non-excluded .md files", () => {
|
|
271
|
-
const output = scanVault(VAULT_ROOT);
|
|
272
|
-
// Should find: My Note.md, projects/Project Alpha.md, daily/2026-04-13.md
|
|
273
|
-
// Should NOT find: short.md (too short), .trash/deleted.md (excluded)
|
|
274
|
-
expect(output.records).toHaveLength(3);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("extracts frontmatter tags", () => {
|
|
278
|
-
const output = scanVault(VAULT_ROOT);
|
|
279
|
-
const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
|
|
280
|
-
expect(myNote).toBeDefined();
|
|
281
|
-
expect(myNote!.tags).toContain("ai");
|
|
282
|
-
expect(myNote!.tags).toContain("memory");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("extracts wikilinks as link: tags", () => {
|
|
286
|
-
const output = scanVault(VAULT_ROOT);
|
|
287
|
-
const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
|
|
288
|
-
expect(myNote).toBeDefined();
|
|
289
|
-
expect(myNote!.tags).toContain("link:RecallNest");
|
|
290
|
-
expect(myNote!.tags).toContain("link:LanceDB");
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("maps folder structure to folder: tags", () => {
|
|
294
|
-
const output = scanVault(VAULT_ROOT);
|
|
295
|
-
const projectNote = output.records.find((r) => r.id?.includes("projects/"));
|
|
296
|
-
expect(projectNote).toBeDefined();
|
|
297
|
-
expect(projectNote!.tags).toContain("folder:projects");
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it("does not add folder: tag for root-level files", () => {
|
|
301
|
-
const output = scanVault(VAULT_ROOT);
|
|
302
|
-
const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
|
|
303
|
-
expect(myNote).toBeDefined();
|
|
304
|
-
const folderTags = myNote!.tags!.filter((t) => t.startsWith("folder:"));
|
|
305
|
-
expect(folderTags).toHaveLength(0);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it("skips .trash and .obsidian directories", () => {
|
|
309
|
-
const output = scanVault(VAULT_ROOT);
|
|
310
|
-
const trashNote = output.records.find((r) => r.id?.includes(".trash"));
|
|
311
|
-
expect(trashNote).toBeUndefined();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("computes contentHash for each record", () => {
|
|
315
|
-
const output = scanVault(VAULT_ROOT);
|
|
316
|
-
for (const record of output.records) {
|
|
317
|
-
expect(record.contentHash).toBeDefined();
|
|
318
|
-
expect(record.contentHash!).toHaveLength(16);
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it("includes sourceMetadata with vault info", () => {
|
|
323
|
-
const output = scanVault(VAULT_ROOT);
|
|
324
|
-
const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
|
|
325
|
-
expect(myNote!.sourceMetadata).toBeDefined();
|
|
326
|
-
expect(myNote!.sourceMetadata!.vaultPath).toBe(VAULT_ROOT);
|
|
327
|
-
expect(myNote!.sourceMetadata!.relativePath).toBe("My Note.md");
|
|
328
|
-
expect(myNote!.sourceMetadata!.frontmatter).toBeDefined();
|
|
329
|
-
expect(myNote!.sourceMetadata!.wikiLinks).toBeDefined();
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("respects custom scopePrefix", () => {
|
|
333
|
-
const output = scanVault(VAULT_ROOT, { scopePrefix: "custom:my-vault" });
|
|
334
|
-
expect(output.scope).toBe("custom:my-vault");
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it("respects custom excludeDirs", () => {
|
|
338
|
-
// Exclude 'daily' directory
|
|
339
|
-
const output = scanVault(VAULT_ROOT, {
|
|
340
|
-
excludeDirs: [".obsidian", ".trash", ".git", "node_modules", "daily"],
|
|
341
|
-
});
|
|
342
|
-
const dailyNote = output.records.find((r) => r.id?.includes("daily/"));
|
|
343
|
-
expect(dailyNote).toBeUndefined();
|
|
344
|
-
// Should still find the other 2
|
|
345
|
-
expect(output.records).toHaveLength(2);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it("strips frontmatter from body text", () => {
|
|
349
|
-
const output = scanVault(VAULT_ROOT);
|
|
350
|
-
const myNote = output.records.find((r) => r.id === "obsidian:My Note.md");
|
|
351
|
-
expect(myNote!.text).not.toContain("---");
|
|
352
|
-
expect(myNote!.text).toContain("# My Note");
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("handles note without frontmatter", () => {
|
|
356
|
-
const output = scanVault(VAULT_ROOT);
|
|
357
|
-
const dailyNote = output.records.find((r) => r.id?.includes("2026-04-13"));
|
|
358
|
-
expect(dailyNote).toBeDefined();
|
|
359
|
-
// Should still extract wikilinks
|
|
360
|
-
expect(dailyNote!.tags).toContain("link:Q2 Goals");
|
|
361
|
-
expect(dailyNote!.tags).toContain("link:PR #123");
|
|
362
|
-
});
|
|
363
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { createRetriever, type RetrievalResultSet } from "../retriever.js";
|
|
4
|
-
|
|
5
|
-
// P-omitted: 验证召回因 limit 上限截断时,对调用方暴露 omitted(提高 limit 可见)。
|
|
6
|
-
// 借鉴 RepoPrompt CE「剪枝必回报」——召回从黑箱变可协商。
|
|
7
|
-
|
|
8
|
-
function buildCandidate(id: string, score: number) {
|
|
9
|
-
return {
|
|
10
|
-
entry: {
|
|
11
|
-
id,
|
|
12
|
-
text: `distinct memory content number ${id}`,
|
|
13
|
-
vector: [1, 0, 0],
|
|
14
|
-
category: "preferences",
|
|
15
|
-
scope: "memory:agent",
|
|
16
|
-
importance: 0.8,
|
|
17
|
-
timestamp: Date.parse("2026-03-16T00:00:00.000Z"),
|
|
18
|
-
metadata: JSON.stringify({}),
|
|
19
|
-
},
|
|
20
|
-
score,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// 最小过滤 config:mode=vector 跳过 BM25/rerank;minScore/hardMinScore=0 + 高分候选避免 hard-min 截断;
|
|
25
|
-
// filterNoise=false;RIF/sourceDiversity 字段缺省即不启用;MMR 只重排不删。
|
|
26
|
-
// 于是过滤链净效果 = sort → slice(limit),omitted = candidates - limit 可精确断言。
|
|
27
|
-
function makeRetriever(
|
|
28
|
-
candidates: ReturnType<typeof buildCandidate>[],
|
|
29
|
-
configOverride: Record<string, unknown> = {},
|
|
30
|
-
) {
|
|
31
|
-
return createRetriever({
|
|
32
|
-
hasFtsSupport: false,
|
|
33
|
-
// 模拟真实 MemoryStore.vectorSearch:尊重 k 参数(按 k 截断返回)。
|
|
34
|
-
// 这样测试能真实反映 over-fetch 是否生效——若 vector-only 不 over-fetch、只取 limit 条,
|
|
35
|
-
// 候选池 ≤ limit、omitted 必为 0,下面断言会失败(Codex review 暴露的假绿点)。
|
|
36
|
-
async vectorSearch(_vec: number[], k: number) { return candidates.slice(0, k); },
|
|
37
|
-
} as any, {
|
|
38
|
-
async embedQuery() { return [1, 0, 0]; },
|
|
39
|
-
async embedPassage() { return [1, 0, 0]; },
|
|
40
|
-
} as any, {
|
|
41
|
-
mode: "vector",
|
|
42
|
-
rerank: "none",
|
|
43
|
-
filterNoise: false,
|
|
44
|
-
hardMinScore: 0,
|
|
45
|
-
minScore: 0,
|
|
46
|
-
recencyWeight: 0,
|
|
47
|
-
timeDecayHalfLifeDays: 0,
|
|
48
|
-
...configOverride,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe("retrieval omitted reporting (P-omitted)", () => {
|
|
53
|
-
it("reports omitted count + reason when candidates exceed limit", async () => {
|
|
54
|
-
const candidates = Array.from({ length: 8 }, (_, i) => buildCandidate(`m${i}`, 0.9 - i * 0.01));
|
|
55
|
-
const retriever = makeRetriever(candidates);
|
|
56
|
-
|
|
57
|
-
const results = await retriever.retrieve({ query: "user reply style preference detail", limit: 3, category: "preferences" }) as RetrievalResultSet;
|
|
58
|
-
|
|
59
|
-
expect(results).toHaveLength(3);
|
|
60
|
-
expect(results.omitted).toBeDefined();
|
|
61
|
-
expect(results.omitted?.count).toBe(5); // 8 candidates - limit 3
|
|
62
|
-
expect(results.omitted?.reason).toBe("limit");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("leaves omitted undefined when candidates fit within limit", async () => {
|
|
66
|
-
const candidates = Array.from({ length: 2 }, (_, i) => buildCandidate(`m${i}`, 0.9 - i * 0.01));
|
|
67
|
-
const retriever = makeRetriever(candidates);
|
|
68
|
-
|
|
69
|
-
const results = await retriever.retrieve({ query: "user reply style preference detail", limit: 5, category: "preferences" }) as RetrievalResultSet;
|
|
70
|
-
|
|
71
|
-
expect(results).toHaveLength(2);
|
|
72
|
-
expect(results.omitted).toBeUndefined();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("persists omitted through the validity filter (regression: array is rebuilt downstream)", async () => {
|
|
76
|
-
// retrieve() runs filterByValidity which returns a NEW array, dropping any
|
|
77
|
-
// property hung on the result array. omitted must survive via the resultSet snapshot.
|
|
78
|
-
const candidates = Array.from({ length: 6 }, (_, i) => buildCandidate(`m${i}`, 0.9 - i * 0.01));
|
|
79
|
-
const retriever = makeRetriever(candidates);
|
|
80
|
-
|
|
81
|
-
const results = await retriever.retrieve({ query: "user reply style preference detail", limit: 2, category: "preferences" }) as RetrievalResultSet;
|
|
82
|
-
|
|
83
|
-
expect(results).toHaveLength(2);
|
|
84
|
-
expect(results.omitted?.count).toBe(4); // 6 - 2
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("reports omitted even when sourceDiversity caps the diversified pool (P3-1)", async () => {
|
|
88
|
-
// 启用 sourceDiversity 时 applySourceDiversity 先把 diversified 压到 limit;
|
|
89
|
-
// omitted 必须从 source-diversity 之前的候选池(clusterDeduped)算,否则恒为 0(Codex 复审 P3-1)。
|
|
90
|
-
const candidates = Array.from({ length: 8 }, (_, i) => buildCandidate(`m${i}`, 0.9 - i * 0.01));
|
|
91
|
-
const retriever = makeRetriever(candidates, { sourceDiversity: 0.5 });
|
|
92
|
-
|
|
93
|
-
const results = await retriever.retrieve({ query: "user reply style preference detail", limit: 3, category: "preferences" }) as RetrievalResultSet;
|
|
94
|
-
|
|
95
|
-
expect(results).toHaveLength(3);
|
|
96
|
-
expect(results.omitted?.count).toBe(5); // 8 - 3,即使 source diversity 压缩了 diversified
|
|
97
|
-
});
|
|
98
|
-
});
|