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.
Files changed (31) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.en.md +2 -1
  3. package/README.md +2 -1
  4. package/docs/en/commands/evidence-embeddings.md +243 -0
  5. package/docs/en/commands/mcp.md +67 -7
  6. package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
  7. package/docs/pt-BR/commands/mcp.md +66 -7
  8. package/hooks/evidence-context.mjs +41 -7
  9. package/hooks/evidence-recall.mjs +10 -0
  10. package/package.json +1 -1
  11. package/packages/mcp/src/effects.mjs +3 -2
  12. package/packages/mcp/src/evidence-recall.mjs +130 -0
  13. package/packages/mcp/src/executor.mjs +4 -0
  14. package/packages/mcp/src/server.mjs +31 -1
  15. package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
  16. package/packages/vault/src/evidence-index-store.mjs +360 -0
  17. package/packages/vault/src/evidence-recall-page.mjs +381 -0
  18. package/packages/vault/src/evidence-search-index.mjs +917 -0
  19. package/packages/vault/src/index.mjs +12 -1
  20. package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
  21. package/packages/vault/src/memory-ledger-view.mjs +41 -0
  22. package/packages/vault/src/memory-rotation-store.mjs +967 -0
  23. package/packages/vault/src/memory-segment-store.mjs +820 -0
  24. package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
  25. package/packages/vault/src/memory-store-base.mjs +1161 -0
  26. package/packages/vault/src/memory-store-core.mjs +2 -0
  27. package/packages/vault/src/memory-store.mjs +46 -1161
  28. package/src/doctor.mjs +41 -5
  29. package/src/evidence-search-health.mjs +221 -0
  30. package/src/memory-scale-health.mjs +210 -0
  31. 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
+ }