memorix 1.1.13 → 1.2.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 +19 -0
- package/README.md +3 -2
- package/README.zh-CN.md +3 -2
- package/dist/cli/index.js +36313 -31189
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +50 -30
- package/dist/index.js +5348 -674
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +3661 -293
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +19 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +5346 -672
- package/dist/sdk.js.map +1 -1
- package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
- package/docs/1.2.0-CODE-STATE.md +61 -0
- package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
- package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
- package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
- package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
- package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
- package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
- package/docs/1.2.0-PRODUCT-STORY.md +234 -0
- package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
- package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
- package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +6 -3
- package/docs/API_REFERENCE.md +25 -6
- package/docs/CONFIGURATION.md +21 -3
- package/docs/README.md +17 -2
- package/docs/dev-log/progress.txt +120 -40
- package/llms-full.txt +16 -2
- package/llms.txt +9 -4
- package/package.json +3 -2
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/commands/codegraph.ts +112 -9
- package/src/cli/commands/context.ts +2 -0
- package/src/cli/commands/doctor.ts +73 -4
- package/src/cli/commands/knowledge.ts +282 -0
- package/src/cli/commands/serve-http.ts +12 -1
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/App.tsx +1 -1
- package/src/cli/tui/Panels.tsx +8 -8
- package/src/cli/tui/theme.ts +1 -1
- package/src/cli/tui/views/GraphView.tsx +8 -7
- package/src/cli/tui/views/KnowledgeView.tsx +9 -9
- package/src/codegraph/auto-context.ts +171 -9
- package/src/codegraph/code-state.ts +95 -0
- package/src/codegraph/context-pack.ts +82 -1
- package/src/codegraph/external-provider.ts +581 -0
- package/src/codegraph/lite-provider.ts +64 -19
- package/src/codegraph/project-context.ts +9 -1
- package/src/codegraph/store.ts +154 -6
- package/src/codegraph/types.ts +117 -0
- package/src/config/resolved-config.ts +28 -0
- package/src/config/toml-loader.ts +3 -0
- package/src/config/yaml-loader.ts +6 -0
- package/src/dashboard/server.ts +15 -1
- package/src/evaluation/workset-evaluation.ts +120 -0
- package/src/hooks/handler.ts +48 -6
- package/src/knowledge/claim-store.ts +267 -0
- package/src/knowledge/claims.ts +537 -0
- package/src/knowledge/markdown.ts +129 -0
- package/src/knowledge/types.ts +157 -0
- package/src/knowledge/wiki.ts +524 -0
- package/src/knowledge/workflow-store.ts +168 -0
- package/src/knowledge/workflow-types.ts +95 -0
- package/src/knowledge/workflows.ts +743 -0
- package/src/knowledge/workset.ts +515 -0
- package/src/knowledge/workspace-store.ts +220 -0
- package/src/knowledge/workspace-types.ts +106 -0
- package/src/knowledge/workspace.ts +220 -0
- package/src/memory/observations.ts +19 -0
- package/src/runtime/control-plane-maintenance.ts +5 -0
- package/src/runtime/isolated-maintenance.ts +5 -0
- package/src/runtime/lifecycle-status.ts +102 -0
- package/src/runtime/lifecycle.ts +107 -0
- package/src/runtime/maintenance-jobs.ts +5 -0
- package/src/runtime/project-maintenance.ts +190 -0
- package/src/server/tool-profile.ts +3 -2
- package/src/server.ts +354 -14
- package/src/store/file-lock.ts +24 -4
- package/src/store/sqlite-db.ts +307 -0
- package/src/wiki/generator.ts +4 -2
- package/src/wiki/knowledge-graph.ts +7 -4
- package/src/wiki/types.ts +16 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
|
|
3
3
|
import { join, relative } from 'node:path';
|
|
4
|
-
import type { CodeEdge, CodeFile, CodeSymbol } from './types.js';
|
|
4
|
+
import type { CodeEdge, CodeFile, CodeStateSnapshot, CodeSymbol } from './types.js';
|
|
5
|
+
import { collectCodeStateSnapshot } from './code-state.js';
|
|
5
6
|
import { makeCodeEdgeId, makeCodeFileId, makeCodeSymbolId, normalizeCodePath } from './ids.js';
|
|
6
7
|
import { isCodeGraphExcludedPath, normalizeCodeGraphExcludePatterns } from './exclude.js';
|
|
7
8
|
import { CodeGraphStore, type CodeGraphFileDelta } from './store.js';
|
|
@@ -19,6 +20,7 @@ export interface LiteIndexResult {
|
|
|
19
20
|
symbols: CodeSymbol[];
|
|
20
21
|
edges: CodeEdge[];
|
|
21
22
|
skippedOversizedFiles: number;
|
|
23
|
+
unreadableFiles: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export interface LiteRefreshResult {
|
|
@@ -30,7 +32,9 @@ export interface LiteRefreshResult {
|
|
|
30
32
|
indexedSymbols: number;
|
|
31
33
|
indexedEdges: number;
|
|
32
34
|
skippedOversizedFiles: number;
|
|
35
|
+
unreadableFiles: number;
|
|
33
36
|
removalScanDeferred: boolean;
|
|
37
|
+
snapshot: CodeStateSnapshot;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export const DEFAULT_CODEGRAPH_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
@@ -299,13 +303,18 @@ function extractImportEdges(projectId: string, file: CodeFile, text: string, ind
|
|
|
299
303
|
return edges;
|
|
300
304
|
}
|
|
301
305
|
|
|
306
|
+
interface LiteFileIndexAttempt {
|
|
307
|
+
delta?: CodeGraphFileDelta;
|
|
308
|
+
unreadable: boolean;
|
|
309
|
+
}
|
|
310
|
+
|
|
302
311
|
function indexFileLite(
|
|
303
312
|
projectId: string,
|
|
304
313
|
projectRoot: string,
|
|
305
314
|
abs: string,
|
|
306
315
|
indexedAt: string,
|
|
307
316
|
fileStat?: ReturnType<typeof statSync>,
|
|
308
|
-
):
|
|
317
|
+
): LiteFileIndexAttempt {
|
|
309
318
|
const rel = normalizeCodePath(relative(projectRoot, abs));
|
|
310
319
|
try {
|
|
311
320
|
const text = readFileSync(abs, 'utf-8');
|
|
@@ -316,17 +325,22 @@ function indexFileLite(
|
|
|
316
325
|
path: rel,
|
|
317
326
|
language: languageForPath(rel),
|
|
318
327
|
contentHash: hashText(text),
|
|
319
|
-
|
|
328
|
+
// Preserve filesystem precision. Rounding here made same-size edits in
|
|
329
|
+
// a short Windows timestamp window indistinguishable to refresh.
|
|
330
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
320
331
|
sizeBytes: Number(stat.size),
|
|
321
332
|
indexedAt,
|
|
322
333
|
};
|
|
323
334
|
return {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
335
|
+
delta: {
|
|
336
|
+
file,
|
|
337
|
+
symbols: extractSymbols(projectId, file, text, indexedAt),
|
|
338
|
+
edges: extractImportEdges(projectId, file, text, indexedAt),
|
|
339
|
+
},
|
|
340
|
+
unreadable: false,
|
|
327
341
|
};
|
|
328
342
|
} catch {
|
|
329
|
-
return
|
|
343
|
+
return { unreadable: true };
|
|
330
344
|
}
|
|
331
345
|
}
|
|
332
346
|
|
|
@@ -340,26 +354,31 @@ export async function indexProjectLite(options: LiteIndexOptions): Promise<LiteI
|
|
|
340
354
|
const symbols: CodeSymbol[] = [];
|
|
341
355
|
const edges: CodeEdge[] = [];
|
|
342
356
|
let skippedOversizedFiles = 0;
|
|
357
|
+
let unreadableFiles = 0;
|
|
343
358
|
|
|
344
359
|
for (const abs of paths) {
|
|
345
360
|
let stat: ReturnType<typeof statSync>;
|
|
346
361
|
try {
|
|
347
362
|
stat = statSync(abs);
|
|
348
363
|
} catch {
|
|
364
|
+
unreadableFiles++;
|
|
349
365
|
continue;
|
|
350
366
|
}
|
|
351
367
|
if (Number(stat.size) > maxFileBytes) {
|
|
352
368
|
skippedOversizedFiles++;
|
|
353
369
|
continue;
|
|
354
370
|
}
|
|
355
|
-
const
|
|
356
|
-
if (!delta)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
371
|
+
const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat);
|
|
372
|
+
if (!attempt.delta) {
|
|
373
|
+
if (attempt.unreadable) unreadableFiles++;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
files.push(attempt.delta.file);
|
|
377
|
+
symbols.push(...attempt.delta.symbols);
|
|
378
|
+
edges.push(...attempt.delta.edges);
|
|
360
379
|
}
|
|
361
380
|
|
|
362
|
-
return { files, symbols, edges, skippedOversizedFiles };
|
|
381
|
+
return { files, symbols, edges, skippedOversizedFiles, unreadableFiles };
|
|
363
382
|
}
|
|
364
383
|
|
|
365
384
|
/**
|
|
@@ -382,31 +401,38 @@ export async function refreshProjectLite(
|
|
|
382
401
|
const oversizedExistingFileIds = new Set<string>();
|
|
383
402
|
let unchangedFiles = 0;
|
|
384
403
|
let skippedOversizedFiles = 0;
|
|
404
|
+
let unreadableFiles = 0;
|
|
385
405
|
|
|
386
406
|
for (const abs of paths) {
|
|
387
407
|
const rel = normalizeCodePath(relative(options.projectRoot, abs));
|
|
408
|
+
// A discovered but temporarily unreadable path is not deletion evidence.
|
|
409
|
+
seenPaths.add(rel);
|
|
388
410
|
let stat: ReturnType<typeof statSync>;
|
|
389
411
|
try {
|
|
390
412
|
stat = statSync(abs);
|
|
391
413
|
} catch {
|
|
414
|
+
unreadableFiles++;
|
|
392
415
|
continue;
|
|
393
416
|
}
|
|
394
|
-
seenPaths.add(rel);
|
|
395
417
|
const existing = existingByPath.get(rel);
|
|
396
|
-
const
|
|
418
|
+
const mtimeMs = Number(stat.mtimeMs);
|
|
397
419
|
const sizeBytes = Number(stat.size);
|
|
398
420
|
if (sizeBytes > maxFileBytes) {
|
|
399
421
|
skippedOversizedFiles++;
|
|
400
422
|
if (existing) oversizedExistingFileIds.add(existing.id);
|
|
401
423
|
continue;
|
|
402
424
|
}
|
|
403
|
-
if (existing && existing.mtimeMs ===
|
|
425
|
+
if (existing && existing.mtimeMs === mtimeMs && existing.sizeBytes === sizeBytes) {
|
|
404
426
|
unchangedFiles++;
|
|
405
427
|
continue;
|
|
406
428
|
}
|
|
407
429
|
|
|
408
|
-
const
|
|
409
|
-
if (!delta)
|
|
430
|
+
const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat);
|
|
431
|
+
if (!attempt.delta) {
|
|
432
|
+
if (attempt.unreadable) unreadableFiles++;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const delta = attempt.delta;
|
|
410
436
|
if (existing?.contentHash === delta.file.contentHash) {
|
|
411
437
|
metadataOnly.push(delta.file);
|
|
412
438
|
unchangedFiles++;
|
|
@@ -430,7 +456,24 @@ export async function refreshProjectLite(
|
|
|
430
456
|
metadataOnly,
|
|
431
457
|
removedFileIds,
|
|
432
458
|
});
|
|
433
|
-
|
|
459
|
+
const completeness = {
|
|
460
|
+
scannedFiles: paths.length,
|
|
461
|
+
maxFiles,
|
|
462
|
+
changedFiles: changed.length,
|
|
463
|
+
unchangedFiles,
|
|
464
|
+
metadataOnlyFiles: metadataOnly.length,
|
|
465
|
+
removedFiles: removedFileIds.length,
|
|
466
|
+
skippedOversizedFiles,
|
|
467
|
+
unreadableFiles,
|
|
468
|
+
removalScanDeferred,
|
|
469
|
+
};
|
|
470
|
+
const snapshot = store.recordCodeStateSnapshot(await collectCodeStateSnapshot({
|
|
471
|
+
projectId: options.projectId,
|
|
472
|
+
projectRoot: options.projectRoot,
|
|
473
|
+
provider: 'lite',
|
|
474
|
+
indexedAt,
|
|
475
|
+
completeness,
|
|
476
|
+
}));
|
|
434
477
|
return {
|
|
435
478
|
scannedFiles: paths.length,
|
|
436
479
|
changedFiles: changed.length,
|
|
@@ -440,6 +483,8 @@ export async function refreshProjectLite(
|
|
|
440
483
|
indexedSymbols: changed.reduce((total, delta) => total + delta.symbols.length, 0),
|
|
441
484
|
indexedEdges: changed.reduce((total, delta) => total + delta.edges.length, 0),
|
|
442
485
|
skippedOversizedFiles,
|
|
486
|
+
unreadableFiles,
|
|
443
487
|
removalScanDeferred,
|
|
488
|
+
snapshot,
|
|
444
489
|
};
|
|
445
490
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { ProjectInfo } from '../types.js';
|
|
2
2
|
import { evaluateCodeRefFreshness } from './freshness.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CodeFile,
|
|
5
|
+
CodeRefStatus,
|
|
6
|
+
CodeStateSnapshot,
|
|
7
|
+
CodeSymbol,
|
|
8
|
+
ObservationCodeRef,
|
|
9
|
+
} from './types.js';
|
|
4
10
|
import type { CodeGraphStore } from './store.js';
|
|
5
11
|
import { isCodeGraphExcludedPath } from './exclude.js';
|
|
6
12
|
|
|
@@ -31,6 +37,7 @@ export interface ProjectContextOverview {
|
|
|
31
37
|
refs: number;
|
|
32
38
|
indexedAt?: string;
|
|
33
39
|
languages: LanguageSummary[];
|
|
40
|
+
latestSnapshot?: CodeStateSnapshot;
|
|
34
41
|
};
|
|
35
42
|
memory: {
|
|
36
43
|
total: number;
|
|
@@ -196,6 +203,7 @@ function overviewFromGraph(input: {
|
|
|
196
203
|
refs: status.refs,
|
|
197
204
|
...(status.indexedAt ? { indexedAt: status.indexedAt } : {}),
|
|
198
205
|
languages: countLanguages(input.graph.files),
|
|
206
|
+
...(status.latestSnapshot ? { latestSnapshot: status.latestSnapshot } : {}),
|
|
199
207
|
},
|
|
200
208
|
memory: {
|
|
201
209
|
total: input.observations.filter(obs => obs.projectId === input.project.id).length,
|
package/src/codegraph/store.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { getDatabase } from '../store/sqlite-db.js';
|
|
2
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CodeEdge,
|
|
5
|
+
CodeFile,
|
|
6
|
+
CodeGraphStatus,
|
|
7
|
+
CodeStateScanCompleteness,
|
|
8
|
+
CodeStateSnapshot,
|
|
9
|
+
CodeStateSnapshotInput,
|
|
10
|
+
CodeSymbol,
|
|
11
|
+
ObservationCodeRef,
|
|
12
|
+
} from './types.js';
|
|
3
13
|
import { normalizeCodePath } from './ids.js';
|
|
4
14
|
|
|
5
15
|
export interface CodeGraphFileDelta {
|
|
@@ -19,6 +29,8 @@ function rowToFile(row: any): CodeFile {
|
|
|
19
29
|
...(row.sizeBytes != null ? { sizeBytes: row.sizeBytes } : {}),
|
|
20
30
|
indexedAt: row.indexedAt,
|
|
21
31
|
...(row.gitCommit ? { gitCommit: row.gitCommit } : {}),
|
|
32
|
+
...(row.snapshotId ? { snapshotId: row.snapshotId } : {}),
|
|
33
|
+
...(row.sourceEpoch != null ? { sourceEpoch: Number(row.sourceEpoch) } : {}),
|
|
22
34
|
};
|
|
23
35
|
}
|
|
24
36
|
|
|
@@ -37,6 +49,8 @@ function rowToSymbol(row: any): CodeSymbol {
|
|
|
37
49
|
...(row.contentHash ? { contentHash: row.contentHash } : {}),
|
|
38
50
|
indexedAt: row.indexedAt,
|
|
39
51
|
stale: !!row.stale,
|
|
52
|
+
...(row.snapshotId ? { snapshotId: row.snapshotId } : {}),
|
|
53
|
+
...(row.sourceEpoch != null ? { sourceEpoch: Number(row.sourceEpoch) } : {}),
|
|
40
54
|
} as CodeSymbol;
|
|
41
55
|
}
|
|
42
56
|
|
|
@@ -52,6 +66,8 @@ function rowToEdge(row: any): CodeEdge {
|
|
|
52
66
|
confidence: row.confidence,
|
|
53
67
|
...(row.evidence ? { evidence: row.evidence } : {}),
|
|
54
68
|
indexedAt: row.indexedAt,
|
|
69
|
+
...(row.snapshotId ? { snapshotId: row.snapshotId } : {}),
|
|
70
|
+
...(row.sourceEpoch != null ? { sourceEpoch: Number(row.sourceEpoch) } : {}),
|
|
55
71
|
} as CodeEdge;
|
|
56
72
|
}
|
|
57
73
|
|
|
@@ -68,16 +84,71 @@ function rowToRef(row: any): ObservationCodeRef {
|
|
|
68
84
|
...(row.reason ? { reason: row.reason } : {}),
|
|
69
85
|
createdAt: row.createdAt,
|
|
70
86
|
...(row.updatedAt ? { updatedAt: row.updatedAt } : {}),
|
|
87
|
+
...(row.snapshotId ? { snapshotId: row.snapshotId } : {}),
|
|
71
88
|
} as ObservationCodeRef;
|
|
72
89
|
}
|
|
73
90
|
|
|
91
|
+
function parseCompleteness(raw: unknown): CodeStateScanCompleteness {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
94
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('invalid completeness');
|
|
95
|
+
return {
|
|
96
|
+
scannedFiles: Number((parsed as any).scannedFiles) || 0,
|
|
97
|
+
maxFiles: Number((parsed as any).maxFiles) || 0,
|
|
98
|
+
changedFiles: Number((parsed as any).changedFiles) || 0,
|
|
99
|
+
unchangedFiles: Number((parsed as any).unchangedFiles) || 0,
|
|
100
|
+
metadataOnlyFiles: Number((parsed as any).metadataOnlyFiles) || 0,
|
|
101
|
+
removedFiles: Number((parsed as any).removedFiles) || 0,
|
|
102
|
+
skippedOversizedFiles: Number((parsed as any).skippedOversizedFiles) || 0,
|
|
103
|
+
unreadableFiles: Number((parsed as any).unreadableFiles) || 0,
|
|
104
|
+
removalScanDeferred: Boolean((parsed as any).removalScanDeferred),
|
|
105
|
+
};
|
|
106
|
+
} catch {
|
|
107
|
+
return {
|
|
108
|
+
scannedFiles: 0,
|
|
109
|
+
maxFiles: 0,
|
|
110
|
+
changedFiles: 0,
|
|
111
|
+
unchangedFiles: 0,
|
|
112
|
+
metadataOnlyFiles: 0,
|
|
113
|
+
removedFiles: 0,
|
|
114
|
+
skippedOversizedFiles: 0,
|
|
115
|
+
unreadableFiles: 0,
|
|
116
|
+
removalScanDeferred: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function rowToSnapshot(row: any): CodeStateSnapshot {
|
|
122
|
+
return {
|
|
123
|
+
id: row.id,
|
|
124
|
+
projectId: row.projectId,
|
|
125
|
+
provider: row.provider,
|
|
126
|
+
...(row.baseRevision ? { baseRevision: row.baseRevision } : {}),
|
|
127
|
+
worktreeFingerprint: row.worktreeFingerprint,
|
|
128
|
+
worktreeState: row.worktreeState,
|
|
129
|
+
changedPathCount: Number(row.changedPathCount),
|
|
130
|
+
indexedAt: row.indexedAt,
|
|
131
|
+
sourceEpoch: Number(row.sourceEpoch),
|
|
132
|
+
completeness: parseCompleteness(row.completenessJson),
|
|
133
|
+
...(row.previousSnapshotId ? { previousSnapshotId: row.previousSnapshotId } : {}),
|
|
134
|
+
} as CodeStateSnapshot;
|
|
135
|
+
}
|
|
136
|
+
|
|
74
137
|
export class CodeGraphStore {
|
|
75
138
|
private db: any = null;
|
|
139
|
+
private dataDir: string | null = null;
|
|
76
140
|
|
|
77
141
|
async init(dataDir: string): Promise<void> {
|
|
142
|
+
this.dataDir = dataDir;
|
|
78
143
|
this.db = getDatabase(dataDir);
|
|
79
144
|
}
|
|
80
145
|
|
|
146
|
+
/** Data directory shared with the rest of the local project evidence stores. */
|
|
147
|
+
getDataDir(): string {
|
|
148
|
+
if (!this.dataDir) throw new Error('CodeGraphStore is not initialized');
|
|
149
|
+
return this.dataDir;
|
|
150
|
+
}
|
|
151
|
+
|
|
81
152
|
upsertFiles(files: CodeFile[]): void {
|
|
82
153
|
if (files.length === 0) return;
|
|
83
154
|
const stmt = this.db.prepare(`
|
|
@@ -364,9 +435,9 @@ export class CodeGraphStore {
|
|
|
364
435
|
if (refs.length === 0) return;
|
|
365
436
|
const stmt = this.db.prepare(`
|
|
366
437
|
INSERT OR REPLACE INTO observation_code_refs
|
|
367
|
-
(id, projectId, observationId, fileId, symbolId, capturedFileHash, capturedSymbolHash, status, reason, createdAt, updatedAt)
|
|
438
|
+
(id, projectId, observationId, fileId, symbolId, capturedFileHash, capturedSymbolHash, status, reason, createdAt, updatedAt, snapshotId)
|
|
368
439
|
VALUES
|
|
369
|
-
(@id, @projectId, @observationId, @fileId, @symbolId, @capturedFileHash, @capturedSymbolHash, @status, @reason, @createdAt, @updatedAt)
|
|
440
|
+
(@id, @projectId, @observationId, @fileId, @symbolId, @capturedFileHash, @capturedSymbolHash, @status, @reason, @createdAt, @updatedAt, @snapshotId)
|
|
370
441
|
`);
|
|
371
442
|
const tx = this.db.transaction((items: ObservationCodeRef[]) => {
|
|
372
443
|
for (const ref of items) {
|
|
@@ -382,6 +453,7 @@ export class CodeGraphStore {
|
|
|
382
453
|
reason: ref.reason ?? null,
|
|
383
454
|
createdAt: ref.createdAt,
|
|
384
455
|
updatedAt: ref.updatedAt ?? null,
|
|
456
|
+
snapshotId: ref.snapshotId ?? this.latestSnapshot(ref.projectId)?.id ?? null,
|
|
385
457
|
});
|
|
386
458
|
}
|
|
387
459
|
});
|
|
@@ -395,9 +467,9 @@ export class CodeGraphStore {
|
|
|
395
467
|
`);
|
|
396
468
|
const insertRef = this.db.prepare(`
|
|
397
469
|
INSERT OR REPLACE INTO observation_code_refs
|
|
398
|
-
(id, projectId, observationId, fileId, symbolId, capturedFileHash, capturedSymbolHash, status, reason, createdAt, updatedAt)
|
|
470
|
+
(id, projectId, observationId, fileId, symbolId, capturedFileHash, capturedSymbolHash, status, reason, createdAt, updatedAt, snapshotId)
|
|
399
471
|
VALUES
|
|
400
|
-
(@id, @projectId, @observationId, @fileId, @symbolId, @capturedFileHash, @capturedSymbolHash, @status, @reason, @createdAt, @updatedAt)
|
|
472
|
+
(@id, @projectId, @observationId, @fileId, @symbolId, @capturedFileHash, @capturedSymbolHash, @status, @reason, @createdAt, @updatedAt, @snapshotId)
|
|
401
473
|
`);
|
|
402
474
|
const tx = this.db.transaction(() => {
|
|
403
475
|
deleteRefs.run(projectId, observationId);
|
|
@@ -414,6 +486,7 @@ export class CodeGraphStore {
|
|
|
414
486
|
reason: ref.reason ?? null,
|
|
415
487
|
createdAt: ref.createdAt,
|
|
416
488
|
updatedAt: ref.updatedAt ?? null,
|
|
489
|
+
snapshotId: ref.snapshotId ?? this.latestSnapshot(ref.projectId)?.id ?? null,
|
|
417
490
|
});
|
|
418
491
|
}
|
|
419
492
|
});
|
|
@@ -514,12 +587,87 @@ export class CodeGraphStore {
|
|
|
514
587
|
`).all(projectId, projectId).map(rowToSymbol);
|
|
515
588
|
}
|
|
516
589
|
|
|
590
|
+
latestSnapshot(projectId: string): CodeStateSnapshot | undefined {
|
|
591
|
+
const row = this.db.prepare(
|
|
592
|
+
'SELECT * FROM code_state_snapshots WHERE projectId = ? ORDER BY sourceEpoch DESC LIMIT 1',
|
|
593
|
+
).get(projectId);
|
|
594
|
+
return row ? rowToSnapshot(row) : undefined;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
listSnapshots(projectId: string, limit = 20): CodeStateSnapshot[] {
|
|
598
|
+
const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(200, Math.floor(limit))) : 20;
|
|
599
|
+
return this.db.prepare(
|
|
600
|
+
'SELECT * FROM code_state_snapshots WHERE projectId = ? ORDER BY sourceEpoch DESC LIMIT ?',
|
|
601
|
+
).all(projectId, safeLimit).map(rowToSnapshot);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Record a completed scan and mark all current structural facts with its
|
|
606
|
+
* epoch. The snapshot is written only after refresh reconciliation succeeds,
|
|
607
|
+
* so an interrupted scan cannot be advertised as complete.
|
|
608
|
+
*/
|
|
609
|
+
recordCodeStateSnapshot(input: CodeStateSnapshotInput): CodeStateSnapshot {
|
|
610
|
+
const id = randomUUID();
|
|
611
|
+
let snapshot: CodeStateSnapshot | undefined;
|
|
612
|
+
const tx = this.db.transaction(() => {
|
|
613
|
+
const previous = this.db.prepare(
|
|
614
|
+
'SELECT id, sourceEpoch FROM code_state_snapshots WHERE projectId = ? ORDER BY sourceEpoch DESC LIMIT 1',
|
|
615
|
+
).get(input.projectId);
|
|
616
|
+
const sourceEpoch = Number(previous?.sourceEpoch ?? 0) + 1;
|
|
617
|
+
const previousSnapshotId = previous?.id as string | undefined;
|
|
618
|
+
this.db.prepare(
|
|
619
|
+
'INSERT INTO code_state_snapshots (id, projectId, provider, baseRevision, worktreeFingerprint, worktreeState, changedPathCount, indexedAt, sourceEpoch, completenessJson, previousSnapshotId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
620
|
+
).run(
|
|
621
|
+
id,
|
|
622
|
+
input.projectId,
|
|
623
|
+
input.provider,
|
|
624
|
+
input.baseRevision ?? null,
|
|
625
|
+
input.worktreeFingerprint,
|
|
626
|
+
input.worktreeState,
|
|
627
|
+
input.changedPathCount,
|
|
628
|
+
input.indexedAt,
|
|
629
|
+
sourceEpoch,
|
|
630
|
+
JSON.stringify(input.completeness),
|
|
631
|
+
previousSnapshotId ?? null,
|
|
632
|
+
);
|
|
633
|
+
this.db.prepare(
|
|
634
|
+
'UPDATE code_files SET snapshotId = ?, sourceEpoch = ?, gitCommit = COALESCE(?, gitCommit) WHERE projectId = ?',
|
|
635
|
+
).run(id, sourceEpoch, input.baseRevision ?? null, input.projectId);
|
|
636
|
+
this.db.prepare(
|
|
637
|
+
'UPDATE code_symbols SET snapshotId = ?, sourceEpoch = ? WHERE projectId = ? AND stale = 0',
|
|
638
|
+
).run(id, sourceEpoch, input.projectId);
|
|
639
|
+
this.db.prepare(
|
|
640
|
+
'UPDATE code_edges SET snapshotId = ?, sourceEpoch = ? WHERE projectId = ?',
|
|
641
|
+
).run(id, sourceEpoch, input.projectId);
|
|
642
|
+
this.db.prepare(
|
|
643
|
+
"UPDATE observation_code_refs SET snapshotId = ? WHERE projectId = ? AND status = 'current'",
|
|
644
|
+
).run(id, input.projectId);
|
|
645
|
+
snapshot = {
|
|
646
|
+
...input,
|
|
647
|
+
id,
|
|
648
|
+
sourceEpoch,
|
|
649
|
+
...(previousSnapshotId ? { previousSnapshotId } : {}),
|
|
650
|
+
};
|
|
651
|
+
});
|
|
652
|
+
tx();
|
|
653
|
+
return snapshot!;
|
|
654
|
+
}
|
|
655
|
+
|
|
517
656
|
status(projectId: string): CodeGraphStatus {
|
|
518
657
|
const files = this.db.prepare(`SELECT COUNT(*) AS count FROM code_files WHERE projectId = ?`).get(projectId).count;
|
|
519
658
|
const symbols = this.db.prepare(`SELECT COUNT(*) AS count FROM code_symbols WHERE projectId = ? AND stale = 0`).get(projectId).count;
|
|
520
659
|
const edges = this.db.prepare(`SELECT COUNT(*) AS count FROM code_edges WHERE projectId = ?`).get(projectId).count;
|
|
521
660
|
const refs = this.db.prepare(`SELECT COUNT(*) AS count FROM observation_code_refs WHERE projectId = ?`).get(projectId).count;
|
|
522
661
|
const latest = this.db.prepare(`SELECT MAX(indexedAt) AS indexedAt FROM code_files WHERE projectId = ?`).get(projectId);
|
|
523
|
-
|
|
662
|
+
const latestSnapshot = this.latestSnapshot(projectId);
|
|
663
|
+
return {
|
|
664
|
+
provider: 'lite',
|
|
665
|
+
files,
|
|
666
|
+
symbols,
|
|
667
|
+
edges,
|
|
668
|
+
refs,
|
|
669
|
+
...(latest?.indexedAt ? { indexedAt: latest.indexedAt } : {}),
|
|
670
|
+
...(latestSnapshot ? { latestSnapshot } : {}),
|
|
671
|
+
};
|
|
524
672
|
}
|
|
525
673
|
}
|
package/src/codegraph/types.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface CodeFile {
|
|
|
32
32
|
sizeBytes?: number;
|
|
33
33
|
indexedAt: string;
|
|
34
34
|
gitCommit?: string;
|
|
35
|
+
snapshotId?: string;
|
|
36
|
+
sourceEpoch?: number;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface CodeSymbol {
|
|
@@ -48,6 +50,8 @@ export interface CodeSymbol {
|
|
|
48
50
|
contentHash?: string;
|
|
49
51
|
indexedAt: string;
|
|
50
52
|
stale?: boolean;
|
|
53
|
+
snapshotId?: string;
|
|
54
|
+
sourceEpoch?: number;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
export interface CodeEdge {
|
|
@@ -61,6 +65,8 @@ export interface CodeEdge {
|
|
|
61
65
|
confidence: number;
|
|
62
66
|
evidence?: string;
|
|
63
67
|
indexedAt: string;
|
|
68
|
+
snapshotId?: string;
|
|
69
|
+
sourceEpoch?: number;
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
export interface ObservationCodeRef {
|
|
@@ -75,6 +81,39 @@ export interface ObservationCodeRef {
|
|
|
75
81
|
reason?: string;
|
|
76
82
|
createdAt: string;
|
|
77
83
|
updatedAt?: string;
|
|
84
|
+
snapshotId?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type CodeStateWorktreeState = 'clean' | 'dirty' | 'unavailable';
|
|
88
|
+
|
|
89
|
+
export interface CodeStateScanCompleteness {
|
|
90
|
+
scannedFiles: number;
|
|
91
|
+
maxFiles: number;
|
|
92
|
+
changedFiles: number;
|
|
93
|
+
unchangedFiles: number;
|
|
94
|
+
metadataOnlyFiles: number;
|
|
95
|
+
removedFiles: number;
|
|
96
|
+
skippedOversizedFiles: number;
|
|
97
|
+
/** Source paths discovered but unavailable for stat/read during this scan. */
|
|
98
|
+
unreadableFiles?: number;
|
|
99
|
+
removalScanDeferred: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface CodeStateSnapshotInput {
|
|
103
|
+
projectId: string;
|
|
104
|
+
provider: CodeGraphProviderKind;
|
|
105
|
+
baseRevision?: string;
|
|
106
|
+
worktreeFingerprint: string;
|
|
107
|
+
worktreeState: CodeStateWorktreeState;
|
|
108
|
+
changedPathCount: number;
|
|
109
|
+
indexedAt: string;
|
|
110
|
+
completeness: CodeStateScanCompleteness;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CodeStateSnapshot extends CodeStateSnapshotInput {
|
|
114
|
+
id: string;
|
|
115
|
+
sourceEpoch: number;
|
|
116
|
+
previousSnapshotId?: string;
|
|
78
117
|
}
|
|
79
118
|
|
|
80
119
|
export interface CodeGraphStatus {
|
|
@@ -84,4 +123,82 @@ export interface CodeGraphStatus {
|
|
|
84
123
|
edges: number;
|
|
85
124
|
refs: number;
|
|
86
125
|
indexedAt?: string;
|
|
126
|
+
latestSnapshot?: CodeStateSnapshot;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Policy for using a separately installed, local semantic CodeGraph index. */
|
|
130
|
+
export type CodeGraphExternalMode = 'auto' | 'off';
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The external provider state is deliberately more precise than a boolean.
|
|
134
|
+
* A caller can distinguish an absent optional tool from a stale or invalid
|
|
135
|
+
* result without treating either as a fatal failure for Code Memory.
|
|
136
|
+
*/
|
|
137
|
+
export type ExternalCodeGraphState =
|
|
138
|
+
| 'disabled'
|
|
139
|
+
| 'not-detected'
|
|
140
|
+
| 'unavailable'
|
|
141
|
+
| 'not-initialized'
|
|
142
|
+
| 'stale'
|
|
143
|
+
| 'timed-out'
|
|
144
|
+
| 'invalid'
|
|
145
|
+
| 'ready';
|
|
146
|
+
|
|
147
|
+
export interface ExternalCodeGraphHealth {
|
|
148
|
+
state: ExternalCodeGraphState;
|
|
149
|
+
reason?: string;
|
|
150
|
+
indexedFiles?: number;
|
|
151
|
+
indexedNodes?: number;
|
|
152
|
+
indexedEdges?: number;
|
|
153
|
+
languages?: string[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface CodeGraphProviderQuality {
|
|
157
|
+
/** Provider that contributed task-scoped code evidence to this response. */
|
|
158
|
+
selected: CodeGraphProviderKind;
|
|
159
|
+
/** Lite is heuristic; external outlines are only semantic after validation. */
|
|
160
|
+
selectedQuality: 'heuristic' | 'semantic';
|
|
161
|
+
mode: CodeGraphExternalMode;
|
|
162
|
+
lite: {
|
|
163
|
+
quality: 'heuristic';
|
|
164
|
+
capabilities: {
|
|
165
|
+
declarations: boolean;
|
|
166
|
+
importHints: boolean;
|
|
167
|
+
resolvedRelations: false;
|
|
168
|
+
exactLocations: false;
|
|
169
|
+
};
|
|
170
|
+
supportedLanguages: string[];
|
|
171
|
+
};
|
|
172
|
+
external: ExternalCodeGraphHealth;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ExternalCodeGraphSymbol {
|
|
176
|
+
id: string;
|
|
177
|
+
name: string;
|
|
178
|
+
qualifiedName?: string;
|
|
179
|
+
kind: string;
|
|
180
|
+
path: string;
|
|
181
|
+
startLine?: number;
|
|
182
|
+
endLine?: number;
|
|
183
|
+
language?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface ExternalCodeGraphRelation {
|
|
187
|
+
from: ExternalCodeGraphSymbol;
|
|
188
|
+
to: ExternalCodeGraphSymbol;
|
|
189
|
+
kind: string;
|
|
190
|
+
line?: number;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** A bounded, non-source-code semantic outline returned for one task. */
|
|
194
|
+
export interface ExternalCodeGraphOutline {
|
|
195
|
+
provider: 'external';
|
|
196
|
+
entryPoints: ExternalCodeGraphSymbol[];
|
|
197
|
+
relations: ExternalCodeGraphRelation[];
|
|
198
|
+
relatedFiles: string[];
|
|
199
|
+
stats: {
|
|
200
|
+
nodes: number;
|
|
201
|
+
edges: number;
|
|
202
|
+
files: number;
|
|
203
|
+
};
|
|
87
204
|
}
|
|
@@ -55,6 +55,9 @@ export interface ResolvedMemorixConfig {
|
|
|
55
55
|
codegraph: {
|
|
56
56
|
excludePatterns?: string[];
|
|
57
57
|
maxFileBytes?: number;
|
|
58
|
+
externalContext: 'auto' | 'off';
|
|
59
|
+
externalCommand?: string;
|
|
60
|
+
externalTimeoutMs?: number;
|
|
58
61
|
};
|
|
59
62
|
server: {
|
|
60
63
|
transport?: 'stdio' | 'http';
|
|
@@ -156,6 +159,22 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
156
159
|
codegraph: {
|
|
157
160
|
excludePatterns: firstArray(toml.codegraph?.exclude_patterns, yaml.codegraph?.excludePatterns),
|
|
158
161
|
maxFileBytes: firstNumber(toml.codegraph?.max_file_bytes, yaml.codegraph?.maxFileBytes),
|
|
162
|
+
externalContext: first(
|
|
163
|
+
normalizeExternalContext(process.env.MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT),
|
|
164
|
+
toml.codegraph?.external_context,
|
|
165
|
+
yaml.codegraph?.externalContext,
|
|
166
|
+
'auto',
|
|
167
|
+
)!,
|
|
168
|
+
externalCommand: first(
|
|
169
|
+
process.env.MEMORIX_CODEGRAPH_EXTERNAL_COMMAND,
|
|
170
|
+
toml.codegraph?.external_command,
|
|
171
|
+
yaml.codegraph?.externalCommand,
|
|
172
|
+
),
|
|
173
|
+
externalTimeoutMs: firstNumber(
|
|
174
|
+
parseNumber(process.env.MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS),
|
|
175
|
+
toml.codegraph?.external_timeout_ms,
|
|
176
|
+
yaml.codegraph?.externalTimeoutMs,
|
|
177
|
+
),
|
|
159
178
|
},
|
|
160
179
|
server: {
|
|
161
180
|
transport: first(toml.server?.transport, yaml.server?.transport),
|
|
@@ -254,6 +273,9 @@ function getEnvSourceNames(): string[] {
|
|
|
254
273
|
'MEMORIX_EMBEDDING_BASE_URL',
|
|
255
274
|
'MEMORIX_EMBEDDING_MODEL',
|
|
256
275
|
'MEMORIX_EMBEDDING_DIMENSIONS',
|
|
276
|
+
'MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT',
|
|
277
|
+
'MEMORIX_CODEGRAPH_EXTERNAL_COMMAND',
|
|
278
|
+
'MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS',
|
|
257
279
|
'OPENROUTER_API_KEY',
|
|
258
280
|
].filter((name) => process.env[name]);
|
|
259
281
|
}
|
|
@@ -267,3 +289,9 @@ function isOpenRouterUrl(value: string | undefined): boolean {
|
|
|
267
289
|
return /(^|\.)openrouter\.ai(?::|\/|$)/i.test(value);
|
|
268
290
|
}
|
|
269
291
|
}
|
|
292
|
+
|
|
293
|
+
function normalizeExternalContext(value: string | undefined): 'auto' | 'off' | undefined {
|
|
294
|
+
const normalized = value?.trim().toLowerCase();
|
|
295
|
+
if (normalized === 'auto' || normalized === 'off') return normalized;
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
@@ -43,6 +43,9 @@ export interface MemorixTomlConfig {
|
|
|
43
43
|
codegraph?: {
|
|
44
44
|
exclude_patterns?: string[];
|
|
45
45
|
max_file_bytes?: number;
|
|
46
|
+
external_context?: 'auto' | 'off';
|
|
47
|
+
external_command?: string;
|
|
48
|
+
external_timeout_ms?: number;
|
|
46
49
|
};
|
|
47
50
|
server?: {
|
|
48
51
|
transport?: 'stdio' | 'http';
|
|
@@ -70,6 +70,12 @@ export interface MemorixYamlConfig {
|
|
|
70
70
|
excludePatterns?: string[];
|
|
71
71
|
/** Maximum source file size to parse into CodeGraph Memory */
|
|
72
72
|
maxFileBytes?: number;
|
|
73
|
+
/** Use a healthy pre-existing local CodeGraph index when present (default: auto). */
|
|
74
|
+
externalContext?: 'auto' | 'off';
|
|
75
|
+
/** Optional CodeGraph executable path when it is not available on PATH. */
|
|
76
|
+
externalCommand?: string;
|
|
77
|
+
/** Bound for one local semantic context request. */
|
|
78
|
+
externalTimeoutMs?: number;
|
|
73
79
|
};
|
|
74
80
|
|
|
75
81
|
/** Behavior settings */
|