wendkeep 0.85.0 → 0.86.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/CHANGELOG.md +40 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/en/commands/evidence-embeddings.md +243 -0
- package/docs/en/commands/mcp.md +67 -7
- package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
- package/docs/pt-BR/commands/mcp.md +66 -7
- package/hooks/evidence-context.mjs +41 -7
- package/hooks/evidence-recall.mjs +10 -0
- package/package.json +1 -1
- package/packages/mcp/src/effects.mjs +3 -2
- package/packages/mcp/src/evidence-recall.mjs +130 -0
- package/packages/mcp/src/executor.mjs +4 -0
- package/packages/mcp/src/server.mjs +31 -1
- package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
- package/packages/vault/src/evidence-index-store.mjs +360 -0
- package/packages/vault/src/evidence-recall-page.mjs +381 -0
- package/packages/vault/src/evidence-search-index.mjs +917 -0
- package/packages/vault/src/index.mjs +12 -1
- package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
- package/packages/vault/src/memory-ledger-view.mjs +41 -0
- package/packages/vault/src/memory-rotation-store.mjs +967 -0
- package/packages/vault/src/memory-segment-store.mjs +820 -0
- package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
- package/packages/vault/src/memory-store-base.mjs +1161 -0
- package/packages/vault/src/memory-store-core.mjs +2 -0
- package/packages/vault/src/memory-store.mjs +46 -1161
- package/src/doctor.mjs +41 -5
- package/src/evidence-search-health.mjs +221 -0
- package/src/memory-scale-health.mjs +210 -0
- package/src/observer-snapshot.mjs +87 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
EVIDENCE_INDEX_FILE,
|
|
7
|
+
EVIDENCE_INDEX_VERSION,
|
|
8
|
+
chunkMarkdownDocument,
|
|
9
|
+
} from './evidence-recall.mjs';
|
|
10
|
+
import {
|
|
11
|
+
assertVaultPathSafe,
|
|
12
|
+
mkdirVaultPath,
|
|
13
|
+
writeVaultFileAtomic,
|
|
14
|
+
} from './vault-path-safety.mjs';
|
|
15
|
+
|
|
16
|
+
export const EVIDENCE_INDEX_STATE_FILE = 'EVIDENCE_INDEX_STATE.json';
|
|
17
|
+
export const EVIDENCE_INDEX_STATE_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
const EXCLUDED_DIRECTORIES = new Set([
|
|
20
|
+
'.brain', '.git', '.obsidian', '.worktrees', 'node_modules',
|
|
21
|
+
]);
|
|
22
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
23
|
+
|
|
24
|
+
function brainDir(vaultBase) {
|
|
25
|
+
return join(vaultBase, '.brain');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function indexPath(vaultBase) {
|
|
29
|
+
return join(brainDir(vaultBase), EVIDENCE_INDEX_FILE);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function statePath(vaultBase) {
|
|
33
|
+
return join(brainDir(vaultBase), EVIDENCE_INDEX_STATE_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hash(value) {
|
|
37
|
+
return createHash('sha256').update(String(value ?? '')).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function checkedFile(vaultBase, path, label, { allowMissing = true } = {}) {
|
|
41
|
+
return assertVaultPathSafe(vaultBase, path, {
|
|
42
|
+
allowMissing,
|
|
43
|
+
expectedType: 'file',
|
|
44
|
+
label,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readOptionalFile(vaultBase, path, label) {
|
|
49
|
+
let checked = checkedFile(vaultBase, path, label);
|
|
50
|
+
if (!checked.exists) return null;
|
|
51
|
+
checked = checkedFile(vaultBase, checked.target, label, { allowMissing: false });
|
|
52
|
+
return readFileSync(checked.target, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeIfChanged(vaultBase, path, content, label) {
|
|
56
|
+
const current = readOptionalFile(vaultBase, path, label);
|
|
57
|
+
if (current === content) return false;
|
|
58
|
+
writeVaultFileAtomic(vaultBase, path, content, 'utf8', { label });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function projectIdForVault(vaultBase) {
|
|
63
|
+
try {
|
|
64
|
+
const raw = readOptionalFile(
|
|
65
|
+
vaultBase,
|
|
66
|
+
join(brainDir(vaultBase), 'PROJECT.json'),
|
|
67
|
+
'autoridade PROJECT.json do índice de evidências',
|
|
68
|
+
);
|
|
69
|
+
return raw === null ? '' : String(JSON.parse(raw).projectId || '');
|
|
70
|
+
} catch {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function walkMarkdown(root, dir = root, found = []) {
|
|
76
|
+
let entries = [];
|
|
77
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return found; }
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (entry.isSymbolicLink?.()) continue;
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
if (!EXCLUDED_DIRECTORIES.has(entry.name)) walkMarkdown(root, join(dir, entry.name), found);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (entry.isFile() && entry.name.endsWith('.md')) found.push(join(dir, entry.name));
|
|
85
|
+
}
|
|
86
|
+
return found;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function checkedMarkdownSource(vaultBase, path, logicalPath) {
|
|
90
|
+
let checked = checkedFile(
|
|
91
|
+
vaultBase,
|
|
92
|
+
path,
|
|
93
|
+
`documento Markdown ${logicalPath} do índice de evidências`,
|
|
94
|
+
{ allowMissing: false },
|
|
95
|
+
);
|
|
96
|
+
checked = checkedFile(
|
|
97
|
+
vaultBase,
|
|
98
|
+
checked.target,
|
|
99
|
+
`documento Markdown ${logicalPath} do índice de evidências`,
|
|
100
|
+
{ allowMissing: false },
|
|
101
|
+
);
|
|
102
|
+
return checked.target;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function nsText(value, fallbackMs = 0) {
|
|
106
|
+
if (typeof value === 'bigint') return value.toString();
|
|
107
|
+
const milliseconds = Number(fallbackMs || 0);
|
|
108
|
+
return BigInt(Math.max(0, Math.trunc(milliseconds * 1_000_000))).toString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function documentFingerprint(path) {
|
|
112
|
+
const stat = statSync(path, { bigint: true });
|
|
113
|
+
if (!stat.isFile()) return null;
|
|
114
|
+
return {
|
|
115
|
+
size: stat.size.toString(),
|
|
116
|
+
mtime_ns: nsText(stat.mtimeNs, stat.mtimeMs),
|
|
117
|
+
ctime_ns: nsText(stat.ctimeNs, stat.ctimeMs),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sameFingerprint(left, right) {
|
|
122
|
+
return Boolean(left && right)
|
|
123
|
+
&& left.size === right.size
|
|
124
|
+
&& left.mtime_ns === right.mtime_ns
|
|
125
|
+
&& left.ctime_ns === right.ctime_ns;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validChunk(row, projectId) {
|
|
129
|
+
return Boolean(row && typeof row === 'object' && !Array.isArray(row))
|
|
130
|
+
&& row.index_version === EVIDENCE_INDEX_VERSION
|
|
131
|
+
&& typeof row.logical_path === 'string'
|
|
132
|
+
&& typeof row.chunk_id === 'string'
|
|
133
|
+
&& typeof row.content === 'string'
|
|
134
|
+
&& SHA256.test(String(row.content_hash || ''))
|
|
135
|
+
&& Number.isInteger(row.ordinal)
|
|
136
|
+
&& row.ordinal >= 0
|
|
137
|
+
&& String(row.project_id || '') === projectId;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function readIndex(vaultBase, projectId) {
|
|
141
|
+
const raw = readOptionalFile(vaultBase, indexPath(vaultBase), 'índice local de evidências');
|
|
142
|
+
if (raw === null) return { status: 'missing', rows: [] };
|
|
143
|
+
try {
|
|
144
|
+
const rows = raw.split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
145
|
+
if (!rows.every((row) => validChunk(row, projectId))) throw new Error('invalid evidence chunk');
|
|
146
|
+
return { status: 'ok', rows };
|
|
147
|
+
} catch {
|
|
148
|
+
return { status: 'corrupt', rows: [] };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validDocumentState(value) {
|
|
153
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
|
154
|
+
&& typeof value.size === 'string'
|
|
155
|
+
&& typeof value.mtime_ns === 'string'
|
|
156
|
+
&& typeof value.ctime_ns === 'string'
|
|
157
|
+
&& SHA256.test(String(value.content_hash || ''))
|
|
158
|
+
&& SHA256.test(String(value.chunks_hash || ''))
|
|
159
|
+
&& Number.isInteger(value.chunk_count)
|
|
160
|
+
&& value.chunk_count >= 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseState(raw, projectId) {
|
|
164
|
+
if (raw === null) return null;
|
|
165
|
+
try {
|
|
166
|
+
const parsed = JSON.parse(raw);
|
|
167
|
+
if (parsed?.schema_version !== EVIDENCE_INDEX_STATE_VERSION
|
|
168
|
+
|| parsed?.index_version !== EVIDENCE_INDEX_VERSION
|
|
169
|
+
|| String(parsed?.project_id || '') !== projectId
|
|
170
|
+
|| !parsed?.documents
|
|
171
|
+
|| typeof parsed.documents !== 'object'
|
|
172
|
+
|| Array.isArray(parsed.documents)
|
|
173
|
+
|| !Object.values(parsed.documents).every(validDocumentState)) return null;
|
|
174
|
+
return parsed;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function loadEvidenceIndexState(vaultBase) {
|
|
181
|
+
const projectId = projectIdForVault(vaultBase);
|
|
182
|
+
const raw = readOptionalFile(vaultBase, statePath(vaultBase), 'estado incremental do índice de evidências');
|
|
183
|
+
return parseState(raw, projectId);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function groupByLogicalPath(rows) {
|
|
187
|
+
const grouped = new Map();
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
if (!grouped.has(row.logical_path)) grouped.set(row.logical_path, []);
|
|
190
|
+
grouped.get(row.logical_path).push(row);
|
|
191
|
+
}
|
|
192
|
+
for (const values of grouped.values()) {
|
|
193
|
+
values.sort((left, right) => left.ordinal - right.ordinal
|
|
194
|
+
|| left.chunk_id.localeCompare(right.chunk_id));
|
|
195
|
+
}
|
|
196
|
+
return grouped;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function chunksHash(rows) {
|
|
200
|
+
return hash(rows.map((row) => JSON.stringify(row)).join('\n'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function stateMatchesIndex(state, rows) {
|
|
204
|
+
const grouped = groupByLogicalPath(rows);
|
|
205
|
+
for (const logicalPath of grouped.keys()) {
|
|
206
|
+
if (!Object.hasOwn(state.documents, logicalPath)) return false;
|
|
207
|
+
}
|
|
208
|
+
for (const [logicalPath, document] of Object.entries(state.documents)) {
|
|
209
|
+
const documentRows = grouped.get(logicalPath) || [];
|
|
210
|
+
if (documentRows.length !== document.chunk_count
|
|
211
|
+
|| chunksHash(documentRows) !== document.chunks_hash) return false;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sortedRecord(value) {
|
|
217
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function renderState(projectId, documents) {
|
|
221
|
+
return `${JSON.stringify({
|
|
222
|
+
schema_version: EVIDENCE_INDEX_STATE_VERSION,
|
|
223
|
+
index_version: EVIDENCE_INDEX_VERSION,
|
|
224
|
+
project_id: projectId,
|
|
225
|
+
documents: sortedRecord(documents),
|
|
226
|
+
}, null, 2)}\n`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function renderIndex(chunks) {
|
|
230
|
+
return chunks.map((chunk) => JSON.stringify(chunk)).join('\n') + (chunks.length ? '\n' : '');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Refresh the derived local evidence index without re-reading unchanged Markdown.
|
|
235
|
+
* The state sidecar is only a cache hint: missing, corrupt or incompatible state
|
|
236
|
+
* causes a deterministic full rebuild from the Vault authority.
|
|
237
|
+
*/
|
|
238
|
+
export function refreshEvidenceIndex(vaultBase, { force = false } = {}) {
|
|
239
|
+
mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain do índice de evidências' });
|
|
240
|
+
const projectId = projectIdForVault(vaultBase);
|
|
241
|
+
const priorIndex = readIndex(vaultBase, projectId);
|
|
242
|
+
const priorStateRaw = readOptionalFile(
|
|
243
|
+
vaultBase,
|
|
244
|
+
statePath(vaultBase),
|
|
245
|
+
'estado incremental do índice de evidências',
|
|
246
|
+
);
|
|
247
|
+
const priorState = parseState(priorStateRaw, projectId);
|
|
248
|
+
const cacheMatches = priorIndex.status === 'ok'
|
|
249
|
+
&& priorState
|
|
250
|
+
&& stateMatchesIndex(priorState, priorIndex.rows);
|
|
251
|
+
const fullRebuild = Boolean(force || !cacheMatches);
|
|
252
|
+
const priorRows = fullRebuild ? [] : priorIndex.rows;
|
|
253
|
+
const priorByPath = groupByLogicalPath(priorRows);
|
|
254
|
+
const priorDocuments = fullRebuild ? {} : priorState.documents;
|
|
255
|
+
|
|
256
|
+
const chunks = [];
|
|
257
|
+
const documents = {};
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
let readDocuments = 0;
|
|
260
|
+
let reusedDocuments = 0;
|
|
261
|
+
let reindexedDocuments = 0;
|
|
262
|
+
|
|
263
|
+
const files = walkMarkdown(vaultBase)
|
|
264
|
+
.map((path) => ({
|
|
265
|
+
path,
|
|
266
|
+
logicalPath: relative(vaultBase, path).replaceAll('\\', '/'),
|
|
267
|
+
}))
|
|
268
|
+
.sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
269
|
+
|
|
270
|
+
for (const item of files) {
|
|
271
|
+
let sourcePath;
|
|
272
|
+
let fingerprint;
|
|
273
|
+
try {
|
|
274
|
+
sourcePath = checkedMarkdownSource(vaultBase, item.path, item.logicalPath);
|
|
275
|
+
fingerprint = documentFingerprint(sourcePath);
|
|
276
|
+
} catch {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (!fingerprint) continue;
|
|
280
|
+
seen.add(item.logicalPath);
|
|
281
|
+
|
|
282
|
+
const previous = priorDocuments[item.logicalPath];
|
|
283
|
+
const previousChunks = priorByPath.get(item.logicalPath) || [];
|
|
284
|
+
const reusableChunkSet = Boolean(previous)
|
|
285
|
+
&& previousChunks.length === previous.chunk_count
|
|
286
|
+
&& chunksHash(previousChunks) === previous.chunks_hash;
|
|
287
|
+
|
|
288
|
+
if (!fullRebuild && reusableChunkSet && sameFingerprint(previous, fingerprint)) {
|
|
289
|
+
chunks.push(...previousChunks);
|
|
290
|
+
documents[item.logicalPath] = previous;
|
|
291
|
+
reusedDocuments += 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let content;
|
|
296
|
+
try { content = readFileSync(sourcePath, 'utf8'); } catch { continue; }
|
|
297
|
+
readDocuments += 1;
|
|
298
|
+
const contentHash = hash(content);
|
|
299
|
+
|
|
300
|
+
if (!fullRebuild && reusableChunkSet && previous.content_hash === contentHash) {
|
|
301
|
+
chunks.push(...previousChunks);
|
|
302
|
+
documents[item.logicalPath] = {
|
|
303
|
+
...fingerprint,
|
|
304
|
+
content_hash: contentHash,
|
|
305
|
+
chunks_hash: chunksHash(previousChunks),
|
|
306
|
+
chunk_count: previousChunks.length,
|
|
307
|
+
};
|
|
308
|
+
reusedDocuments += 1;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const nextChunks = chunkMarkdownDocument({
|
|
313
|
+
projectId,
|
|
314
|
+
logicalPath: item.logicalPath,
|
|
315
|
+
content,
|
|
316
|
+
});
|
|
317
|
+
chunks.push(...nextChunks);
|
|
318
|
+
documents[item.logicalPath] = {
|
|
319
|
+
...fingerprint,
|
|
320
|
+
content_hash: contentHash,
|
|
321
|
+
chunks_hash: chunksHash(nextChunks),
|
|
322
|
+
chunk_count: nextChunks.length,
|
|
323
|
+
};
|
|
324
|
+
reindexedDocuments += 1;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
chunks.sort((left, right) => left.logical_path.localeCompare(right.logical_path)
|
|
328
|
+
|| left.ordinal - right.ordinal || left.chunk_id.localeCompare(right.chunk_id));
|
|
329
|
+
|
|
330
|
+
const deletedDocuments = Object.keys(priorDocuments)
|
|
331
|
+
.filter((logicalPath) => !seen.has(logicalPath)).length;
|
|
332
|
+
const indexWritten = writeIfChanged(
|
|
333
|
+
vaultBase,
|
|
334
|
+
indexPath(vaultBase),
|
|
335
|
+
renderIndex(chunks),
|
|
336
|
+
'índice local de evidências',
|
|
337
|
+
);
|
|
338
|
+
const stateWritten = writeIfChanged(
|
|
339
|
+
vaultBase,
|
|
340
|
+
statePath(vaultBase),
|
|
341
|
+
renderState(projectId, documents),
|
|
342
|
+
'estado incremental do índice de evidências',
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
chunks,
|
|
347
|
+
documents: Object.keys(documents).length,
|
|
348
|
+
read_documents: readDocuments,
|
|
349
|
+
reused_documents: reusedDocuments,
|
|
350
|
+
reindexed_documents: reindexedDocuments,
|
|
351
|
+
deleted_documents: deletedDocuments,
|
|
352
|
+
full_rebuild: fullRebuild,
|
|
353
|
+
index_written: indexWritten,
|
|
354
|
+
state_written: stateWritten,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function buildIncrementalEvidenceIndex(vaultBase, options = {}) {
|
|
359
|
+
return refreshEvidenceIndex(vaultBase, options).chunks;
|
|
360
|
+
}
|