memorix 1.4.0 → 1.4.2
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 +31 -0
- package/CLAUDE.md +9 -9
- package/README.md +7 -2
- package/README.zh-CN.md +6 -2
- package/dist/cli/index.js +1216 -485
- package/dist/index.js +984 -337
- package/dist/maintenance-runner.js +564 -180
- package/dist/media-video-runner.js +564 -180
- package/dist/memcode/index.js +53 -21
- package/dist/memcode-runtime/CHANGELOG.md +31 -0
- package/dist/sdk.js +983 -336
- package/dist/vector-backfill-runner.js +564 -180
- package/docs/1.4.2-EVIDENCE-GOVERNED-MEMORY-SPEC.md +434 -0
- package/docs/API_REFERENCE.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +52 -128
- package/package.json +5 -2
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/codegraph.ts +83 -2
- package/src/codegraph/auto-context.ts +20 -0
- package/src/codegraph/current-facts.ts +1 -0
- package/src/codegraph/external-provider.ts +1 -0
- package/src/codegraph/store.ts +216 -5
- package/src/codegraph/types.ts +42 -0
- package/src/knowledge/context-assembly.ts +25 -0
- package/src/knowledge/governor.ts +156 -0
- package/src/knowledge/outcome-store.ts +102 -0
- package/src/knowledge/outcome-types.ts +49 -0
- package/src/knowledge/workflows.ts +32 -1
- package/src/knowledge/workset.ts +203 -13
- package/src/store/sqlite-db.ts +42 -0
- package/docs/dev-log/progress.txt +0 -388
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memorix",
|
|
3
3
|
"mcpName": "io.github.AVIDS2/memorix",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.2",
|
|
5
5
|
"description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"README.zh-CN.md",
|
|
45
45
|
"assets",
|
|
46
46
|
"docs",
|
|
47
|
+
"!docs/dev-log",
|
|
47
48
|
"!docs/memcode",
|
|
48
49
|
"!docs/superpowers",
|
|
49
50
|
"plugins",
|
|
@@ -69,9 +70,11 @@
|
|
|
69
70
|
"generate:star-history": "node scripts/generate-star-history.mjs",
|
|
70
71
|
"sync:mcp-registry": "node scripts/sync-mcp-registry-manifest.mjs",
|
|
71
72
|
"check:mcp-registry": "node scripts/check-mcp-registry-manifest.mjs",
|
|
73
|
+
"sync:plugin-release": "node scripts/sync-plugin-release-manifests.mjs",
|
|
74
|
+
"check:plugin-release": "node scripts/sync-plugin-release-manifests.mjs --check",
|
|
72
75
|
"audit:pi-upstream": "node scripts/pi-upstream-audit.mjs",
|
|
73
76
|
"lint": "tsc --noEmit",
|
|
74
|
-
"prepublishOnly": "npm run sync:mcp-registry && npm run check:mcp-registry && npm run build && npm test"
|
|
77
|
+
"prepublishOnly": "npm run sync:mcp-registry && npm run sync:plugin-release && npm run check:mcp-registry && npm run check:plugin-release && npm run build && npm test"
|
|
75
78
|
},
|
|
76
79
|
"keywords": [
|
|
77
80
|
"mcp",
|
package/server.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"url": "https://github.com/AVIDS2/memorix",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.4.
|
|
10
|
+
"version": "1.4.2",
|
|
11
11
|
"websiteUrl": "https://github.com/AVIDS2/memorix#readme",
|
|
12
12
|
"icons": [
|
|
13
13
|
{
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "memorix",
|
|
25
|
-
"version": "1.4.
|
|
25
|
+
"version": "1.4.2",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"packageArguments": [
|
|
28
28
|
{
|
|
@@ -58,12 +58,52 @@ function formatUsageHint(): string {
|
|
|
58
58
|
'Usage:',
|
|
59
59
|
' memorix codegraph refresh',
|
|
60
60
|
' memorix codegraph status --json',
|
|
61
|
+
' memorix codegraph diff [--from <snapshot>] [--to <snapshot>]',
|
|
62
|
+
' memorix codegraph impact --path <relative-source-path>',
|
|
61
63
|
' memorix codegraph context-pack --task "continue auth bug"',
|
|
62
64
|
'',
|
|
63
65
|
'Tip: use `memorix context "..."` for new work or `memorix resume "..."` for prior work.',
|
|
64
66
|
].join('\n');
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
function formatDiff(diff: ReturnType<CodeGraphStore['diffSnapshots']>, impact: ReturnType<CodeGraphStore['impactSlice']>): string {
|
|
70
|
+
if (!diff.available) {
|
|
71
|
+
return `CodeGraph diff is unavailable: ${diff.reason ?? 'unknown reason'}. Refresh twice to establish comparable snapshots.`;
|
|
72
|
+
}
|
|
73
|
+
const summary = diff.changes.reduce((counts, change) => {
|
|
74
|
+
counts[change.kind] += 1;
|
|
75
|
+
return counts;
|
|
76
|
+
}, { added: 0, modified: 0, removed: 0 });
|
|
77
|
+
const lines = [
|
|
78
|
+
`CodeGraph diff: ${diff.fromSnapshotId} -> ${diff.toSnapshotId}`,
|
|
79
|
+
`- Changed files: ${diff.changes.length} (${summary.added} added, ${summary.modified} modified, ${summary.removed} removed)`,
|
|
80
|
+
];
|
|
81
|
+
for (const change of diff.changes.slice(0, 20)) lines.push(`- ${change.kind}: ${change.path}`);
|
|
82
|
+
if (diff.changes.length > 20) lines.push(`- ${diff.changes.length - 20} additional file change(s) omitted`);
|
|
83
|
+
if (impact.directlyConnectedPaths.length > 0) {
|
|
84
|
+
lines.push('', 'Current one-hop structural impact');
|
|
85
|
+
for (const path of impact.directlyConnectedPaths.slice(0, 20)) lines.push(`- ${path}`);
|
|
86
|
+
}
|
|
87
|
+
if (impact.relationCount === 0) lines.push('', 'No current file-edge relation was found for the changed paths.');
|
|
88
|
+
if (impact.truncated) lines.push('- Impact relation limit reached; inspect the graph before assuming coverage.');
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatImpact(impact: ReturnType<CodeGraphStore['impactSlice']>): string {
|
|
93
|
+
const lines = [
|
|
94
|
+
`CodeGraph current one-hop impact: ${impact.changedPaths.join(', ')}`,
|
|
95
|
+
`- Relations inspected: ${impact.relationCount}`,
|
|
96
|
+
];
|
|
97
|
+
if (impact.directlyConnectedPaths.length > 0) {
|
|
98
|
+
lines.push('- Directly connected paths:');
|
|
99
|
+
for (const path of impact.directlyConnectedPaths) lines.push(` - ${path}`);
|
|
100
|
+
} else {
|
|
101
|
+
lines.push('- No current file-edge relation was found for this indexed path.');
|
|
102
|
+
}
|
|
103
|
+
if (impact.truncated) lines.push('- Impact relation limit reached; inspect the graph before assuming coverage.');
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
67
107
|
function compactFacts(project: { rootPath: string }): { facts: string[]; dirty: boolean } {
|
|
68
108
|
const current = collectCurrentProjectFacts({ project, now: new Date() });
|
|
69
109
|
const facts: string[] = [];
|
|
@@ -82,9 +122,13 @@ export default defineCommand({
|
|
|
82
122
|
description: 'Inspect and refresh CodeGraph Memory for the current project',
|
|
83
123
|
},
|
|
84
124
|
args: {
|
|
85
|
-
action: { type: 'string', description: 'Action: status, refresh, or context-pack' },
|
|
125
|
+
action: { type: 'string', description: 'Action: status, refresh, diff, impact, or context-pack' },
|
|
86
126
|
task: { type: 'string', description: 'Task text for context-pack' },
|
|
127
|
+
path: { type: 'string', description: 'Indexed relative source path for impact' },
|
|
87
128
|
limit: { type: 'string', description: 'Max active memories to inspect for context-pack' },
|
|
129
|
+
from: { type: 'string', description: 'Earlier CodeGraph snapshot id for diff' },
|
|
130
|
+
to: { type: 'string', description: 'Later CodeGraph snapshot id for diff' },
|
|
131
|
+
impactLimit: { type: 'string', description: 'Maximum graph relations to inspect for diff impact' },
|
|
88
132
|
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
89
133
|
},
|
|
90
134
|
run: async ({ args }) => {
|
|
@@ -162,6 +206,43 @@ export default defineCommand({
|
|
|
162
206
|
return;
|
|
163
207
|
}
|
|
164
208
|
|
|
209
|
+
case 'diff': {
|
|
210
|
+
const snapshots = store.listSnapshots(project.id, 2);
|
|
211
|
+
const fromSnapshotId = (args.from as string | undefined)?.trim() || snapshots[1]?.id;
|
|
212
|
+
const toSnapshotId = (args.to as string | undefined)?.trim() || snapshots[0]?.id;
|
|
213
|
+
if (!fromSnapshotId || !toSnapshotId) {
|
|
214
|
+
emitError('two completed CodeGraph snapshots are required; run "memorix codegraph refresh" again after a code change.', asJson);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const diff = store.diffSnapshots(project.id, fromSnapshotId, toSnapshotId);
|
|
218
|
+
const impact = store.impactSlice(
|
|
219
|
+
project.id,
|
|
220
|
+
diff.changes.map(change => change.path),
|
|
221
|
+
parsePositiveInt(args.impactLimit as string | undefined, 24),
|
|
222
|
+
);
|
|
223
|
+
emitResult({ project, diff, impact }, formatDiff(diff, impact), asJson);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case 'impact': {
|
|
228
|
+
const sourcePath = (args.path as string | undefined)?.trim() || positional.slice(1).join(' ').trim();
|
|
229
|
+
if (!sourcePath) {
|
|
230
|
+
emitError('path is required for "memorix codegraph impact"', asJson);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!store.getFile(project.id, sourcePath)) {
|
|
234
|
+
emitError(`path is not in the current CodeGraph index: ${sourcePath}. Run "memorix codegraph refresh" and retry.`, asJson);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const impact = store.impactSlice(
|
|
238
|
+
project.id,
|
|
239
|
+
[sourcePath],
|
|
240
|
+
parsePositiveInt(args.impactLimit as string | undefined, 24),
|
|
241
|
+
);
|
|
242
|
+
emitResult({ project, impact }, formatImpact(impact), asJson);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
165
246
|
case 'context-pack': {
|
|
166
247
|
const task = (args.task as string | undefined)?.trim() || positional.slice(1).join(' ').trim();
|
|
167
248
|
if (!task) {
|
|
@@ -224,7 +305,7 @@ export default defineCommand({
|
|
|
224
305
|
}
|
|
225
306
|
|
|
226
307
|
default:
|
|
227
|
-
emitError(`unknown codegraph action "${action}". Use "status", "refresh", or "context-pack".`, asJson);
|
|
308
|
+
emitError(`unknown codegraph action "${action}". Use "status", "refresh", "diff", "impact", or "context-pack".`, asJson);
|
|
228
309
|
}
|
|
229
310
|
} catch (error) {
|
|
230
311
|
emitError(error instanceof Error ? error.message : String(error), asJson);
|
|
@@ -259,6 +259,25 @@ export async function buildAutoProjectContext(input: {
|
|
|
259
259
|
const overview = explain.overview;
|
|
260
260
|
const currentFacts = collectCurrentProjectFacts({ project: input.project, now });
|
|
261
261
|
const latestSnapshot = overview.code.latestSnapshot;
|
|
262
|
+
let codeEvolution: TaskWorkset['codeEvolution'];
|
|
263
|
+
if (continuationRequested && latestSnapshot?.previousSnapshotId && latestSnapshot.changedPathCount > 0) {
|
|
264
|
+
const diff = store.diffSnapshots(input.project.id, latestSnapshot.previousSnapshotId, latestSnapshot.id);
|
|
265
|
+
if (diff.available && diff.changes.length > 0) {
|
|
266
|
+
const impact = store.impactSlice(input.project.id, diff.changes.map(change => change.path));
|
|
267
|
+
const changedFileIds = new Set([
|
|
268
|
+
...store.listSnapshotFiles(diff.fromSnapshotId),
|
|
269
|
+
...store.listSnapshotFiles(diff.toSnapshotId),
|
|
270
|
+
].filter(file => diff.changes.some(change => change.path === file.path)).map(file => file.fileId));
|
|
271
|
+
codeEvolution = {
|
|
272
|
+
fromSnapshotId: diff.fromSnapshotId,
|
|
273
|
+
toSnapshotId: diff.toSnapshotId,
|
|
274
|
+
changes: diff.changes.slice(0, 5).map(change => ({ path: change.path, kind: change.kind })),
|
|
275
|
+
directlyConnectedPaths: impact.directlyConnectedPaths.slice(0, 3),
|
|
276
|
+
affectedMemoryCount: store.countStaleObservationRefsForFiles(input.project.id, [...changedFileIds]),
|
|
277
|
+
truncated: impact.truncated || diff.changes.length > 5,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
262
281
|
const sourceSets = lensSourceSets({ task, lens, explain });
|
|
263
282
|
let externalOutline: ExternalCodeGraphOutline | undefined;
|
|
264
283
|
let externalCaution: string | undefined;
|
|
@@ -348,6 +367,7 @@ export async function buildAutoProjectContext(input: {
|
|
|
348
367
|
currentFacts: worksetFactLines(currentFacts),
|
|
349
368
|
...(continuation ? { continuation } : {}),
|
|
350
369
|
codeState: codeStateLine(overview),
|
|
370
|
+
...(codeEvolution ? { codeEvolution } : {}),
|
|
351
371
|
reliableMemory: sourceSets.reliableSources
|
|
352
372
|
.slice(0, lens.sourceLimit)
|
|
353
373
|
.map(source => ({
|
package/src/codegraph/store.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { posix as pathPosix } from 'node:path';
|
|
2
3
|
import { getDatabase } from '../store/sqlite-db.js';
|
|
3
4
|
import type {
|
|
4
5
|
CodeEdge,
|
|
5
6
|
CodeFile,
|
|
7
|
+
CodeGraphImpactSlice,
|
|
6
8
|
CodeGraphStatus,
|
|
9
|
+
CodeStateDiff,
|
|
10
|
+
CodeStateFileChange,
|
|
11
|
+
CodeStateSnapshotFile,
|
|
7
12
|
CodeStateScanCompleteness,
|
|
8
13
|
CodeStateSnapshot,
|
|
9
14
|
CodeStateSnapshotInput,
|
|
@@ -71,6 +76,41 @@ function rowToEdge(row: any): CodeEdge {
|
|
|
71
76
|
} as CodeEdge;
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
function resolveRelativeImportTarget(
|
|
80
|
+
sourcePath: string,
|
|
81
|
+
evidence: string | undefined,
|
|
82
|
+
fileByPath: Map<string, CodeFile>,
|
|
83
|
+
): CodeFile | undefined {
|
|
84
|
+
const target = evidence?.trim().replace(/\\/g, '/');
|
|
85
|
+
if (!target?.startsWith('.')) return undefined;
|
|
86
|
+
const base = normalizeCodePath(pathPosix.normalize(pathPosix.join(pathPosix.dirname(sourcePath), target)));
|
|
87
|
+
if (!base || base === '..' || base.startsWith('../')) return undefined;
|
|
88
|
+
const candidates = [base];
|
|
89
|
+
const extensionMatch = base.match(/(\.[A-Za-z0-9]+)$/);
|
|
90
|
+
const hasExtension = Boolean(extensionMatch);
|
|
91
|
+
if (!hasExtension) {
|
|
92
|
+
const extensions = [...new Set([...fileByPath.keys()]
|
|
93
|
+
.map(path => path.match(/(\.[A-Za-z0-9]+)$/)?.[1])
|
|
94
|
+
.filter((extension): extension is string => Boolean(extension)))];
|
|
95
|
+
for (const extension of extensions) {
|
|
96
|
+
candidates.push(`${base}${extension}`, `${base}/index${extension}`);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// Node ESM projects commonly import the emitted .js path from a .ts source.
|
|
100
|
+
// Keep an exact path first, then resolve only equivalent local source forms.
|
|
101
|
+
const sourceStem = base.slice(0, -extensionMatch![1].length);
|
|
102
|
+
const sourceExtensions = extensionMatch![1] === '.js'
|
|
103
|
+
? ['.ts', '.tsx', '.js', '.jsx']
|
|
104
|
+
: extensionMatch![1] === '.mjs'
|
|
105
|
+
? ['.mts', '.mjs']
|
|
106
|
+
: extensionMatch![1] === '.cjs'
|
|
107
|
+
? ['.cts', '.cjs']
|
|
108
|
+
: [];
|
|
109
|
+
for (const extension of sourceExtensions) candidates.push(`${sourceStem}${extension}`);
|
|
110
|
+
}
|
|
111
|
+
return candidates.map(candidate => fileByPath.get(candidate)).find((file): file is CodeFile => Boolean(file));
|
|
112
|
+
}
|
|
113
|
+
|
|
74
114
|
function rowToRef(row: any): ObservationCodeRef {
|
|
75
115
|
return {
|
|
76
116
|
id: row.id,
|
|
@@ -134,6 +174,22 @@ function rowToSnapshot(row: any): CodeStateSnapshot {
|
|
|
134
174
|
} as CodeStateSnapshot;
|
|
135
175
|
}
|
|
136
176
|
|
|
177
|
+
function rowToSnapshotFile(row: any): CodeStateSnapshotFile {
|
|
178
|
+
return {
|
|
179
|
+
snapshotId: row.snapshotId,
|
|
180
|
+
projectId: row.projectId,
|
|
181
|
+
fileId: row.fileId,
|
|
182
|
+
path: row.path,
|
|
183
|
+
contentHash: row.contentHash,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function snapshotIsIncomplete(snapshot: CodeStateSnapshot): boolean {
|
|
188
|
+
return snapshot.completeness.skippedOversizedFiles > 0
|
|
189
|
+
|| (snapshot.completeness.unreadableFiles ?? 0) > 0
|
|
190
|
+
|| snapshot.completeness.removalScanDeferred;
|
|
191
|
+
}
|
|
192
|
+
|
|
137
193
|
export class CodeGraphStore {
|
|
138
194
|
private db: any = null;
|
|
139
195
|
private dataDir: string | null = null;
|
|
@@ -312,6 +368,7 @@ export class CodeGraphStore {
|
|
|
312
368
|
});
|
|
313
369
|
|
|
314
370
|
tx();
|
|
371
|
+
this.reconcileLocalImportTargets(projectId);
|
|
315
372
|
}
|
|
316
373
|
|
|
317
374
|
/**
|
|
@@ -329,7 +386,10 @@ export class CodeGraphStore {
|
|
|
329
386
|
const changed = input.changed.filter((delta) => delta.file.projectId === projectId);
|
|
330
387
|
const metadataOnly = (input.metadataOnly ?? []).filter((file) => file.projectId === projectId);
|
|
331
388
|
const removedFileIds = input.removedFileIds ?? [];
|
|
332
|
-
if (changed.length === 0 && metadataOnly.length === 0 && removedFileIds.length === 0)
|
|
389
|
+
if (changed.length === 0 && metadataOnly.length === 0 && removedFileIds.length === 0) {
|
|
390
|
+
this.reconcileLocalImportTargets(projectId);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
333
393
|
|
|
334
394
|
const staleRefsForFile = this.db.prepare(`
|
|
335
395
|
UPDATE observation_code_refs
|
|
@@ -340,9 +400,18 @@ export class CodeGraphStore {
|
|
|
340
400
|
)
|
|
341
401
|
)
|
|
342
402
|
`);
|
|
343
|
-
const
|
|
403
|
+
const deleteEdgesFromFile = this.db.prepare(`
|
|
404
|
+
DELETE FROM code_edges
|
|
405
|
+
WHERE projectId = ? AND fromFileId = ?
|
|
406
|
+
`);
|
|
407
|
+
const clearImportTargetsForFile = this.db.prepare(`
|
|
408
|
+
UPDATE code_edges
|
|
409
|
+
SET toFileId = NULL
|
|
410
|
+
WHERE projectId = ? AND toFileId = ? AND type = 'imports'
|
|
411
|
+
`);
|
|
412
|
+
const deleteOtherIncomingEdgesForFile = this.db.prepare(`
|
|
344
413
|
DELETE FROM code_edges
|
|
345
|
-
WHERE projectId = ? AND
|
|
414
|
+
WHERE projectId = ? AND toFileId = ? AND type != 'imports'
|
|
346
415
|
`);
|
|
347
416
|
const deleteSymbolsForFile = this.db.prepare(`DELETE FROM code_symbols WHERE projectId = ? AND fileId = ?`);
|
|
348
417
|
const deleteFile = this.db.prepare(`DELETE FROM code_files WHERE projectId = ? AND id = ?`);
|
|
@@ -380,7 +449,9 @@ export class CodeGraphStore {
|
|
|
380
449
|
const staleAt = new Date().toISOString();
|
|
381
450
|
for (const fileId of removedFileIds) {
|
|
382
451
|
staleRefsForFile.run(staleAt, projectId, fileId, projectId, fileId);
|
|
383
|
-
|
|
452
|
+
deleteEdgesFromFile.run(projectId, fileId);
|
|
453
|
+
clearImportTargetsForFile.run(projectId, fileId);
|
|
454
|
+
deleteOtherIncomingEdgesForFile.run(projectId, fileId);
|
|
384
455
|
deleteSymbolsForFile.run(projectId, fileId);
|
|
385
456
|
deleteFile.run(projectId, fileId);
|
|
386
457
|
}
|
|
@@ -388,7 +459,9 @@ export class CodeGraphStore {
|
|
|
388
459
|
for (const delta of changed) {
|
|
389
460
|
const fileId = delta.file.id;
|
|
390
461
|
staleRefsForFile.run(staleAt, projectId, fileId, projectId, fileId);
|
|
391
|
-
|
|
462
|
+
deleteEdgesFromFile.run(projectId, fileId);
|
|
463
|
+
clearImportTargetsForFile.run(projectId, fileId);
|
|
464
|
+
deleteOtherIncomingEdgesForFile.run(projectId, fileId);
|
|
392
465
|
deleteSymbolsForFile.run(projectId, fileId);
|
|
393
466
|
writeFile(delta.file);
|
|
394
467
|
|
|
@@ -429,6 +502,7 @@ export class CodeGraphStore {
|
|
|
429
502
|
for (const file of metadataOnly) writeFile(file);
|
|
430
503
|
});
|
|
431
504
|
tx();
|
|
505
|
+
this.reconcileLocalImportTargets(projectId);
|
|
432
506
|
}
|
|
433
507
|
|
|
434
508
|
upsertObservationRefs(refs: ObservationCodeRef[]): void {
|
|
@@ -493,6 +567,26 @@ export class CodeGraphStore {
|
|
|
493
567
|
tx();
|
|
494
568
|
}
|
|
495
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Lite import extraction records the original import text first. Resolve only
|
|
572
|
+
* local relative imports against the current index after each refresh, so an
|
|
573
|
+
* added or removed target cannot leave a fabricated file-to-file relation.
|
|
574
|
+
*/
|
|
575
|
+
reconcileLocalImportTargets(projectId: string): void {
|
|
576
|
+
const fileById = new Map(this.listFiles(projectId).map(file => [file.id, file]));
|
|
577
|
+
const fileByPath = new Map([...fileById.values()].map(file => [file.path, file]));
|
|
578
|
+
const update = this.db.prepare('UPDATE code_edges SET toFileId = ? WHERE id = ?');
|
|
579
|
+
const tx = this.db.transaction(() => {
|
|
580
|
+
for (const edge of this.listEdges(projectId)) {
|
|
581
|
+
if (edge.type !== 'imports' || !edge.fromFileId) continue;
|
|
582
|
+
const source = fileById.get(edge.fromFileId);
|
|
583
|
+
const target = source ? resolveRelativeImportTarget(source.path, edge.evidence, fileByPath) : undefined;
|
|
584
|
+
update.run(target?.id ?? null, edge.id);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
tx();
|
|
588
|
+
}
|
|
589
|
+
|
|
496
590
|
getFile(projectId: string, path: string): CodeFile | null {
|
|
497
591
|
const row = this.db.prepare(`SELECT * FROM code_files WHERE projectId = ? AND path = ?`).get(projectId, normalizeCodePath(path));
|
|
498
592
|
return row ? rowToFile(row) : null;
|
|
@@ -577,6 +671,19 @@ export class CodeGraphStore {
|
|
|
577
671
|
`).all(projectId).map(rowToRef);
|
|
578
672
|
}
|
|
579
673
|
|
|
674
|
+
/** Count distinct observations whose binding to these exact files is stale. */
|
|
675
|
+
countStaleObservationRefsForFiles(projectId: string, fileIds: string[]): number {
|
|
676
|
+
const ids = [...new Set(fileIds.filter(Boolean))];
|
|
677
|
+
if (ids.length === 0) return 0;
|
|
678
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
679
|
+
const row = this.db.prepare(`
|
|
680
|
+
SELECT COUNT(DISTINCT observationId) AS count
|
|
681
|
+
FROM observation_code_refs
|
|
682
|
+
WHERE projectId = ? AND status = 'stale' AND fileId IN (${placeholders})
|
|
683
|
+
`).get(projectId, ...ids);
|
|
684
|
+
return Number(row?.count ?? 0);
|
|
685
|
+
}
|
|
686
|
+
|
|
580
687
|
listReferencedSymbols(projectId: string): CodeSymbol[] {
|
|
581
688
|
return this.db.prepare(`
|
|
582
689
|
SELECT DISTINCT symbols.*
|
|
@@ -601,6 +708,100 @@ export class CodeGraphStore {
|
|
|
601
708
|
).all(projectId, safeLimit).map(rowToSnapshot);
|
|
602
709
|
}
|
|
603
710
|
|
|
711
|
+
listSnapshotFiles(snapshotId: string): CodeStateSnapshotFile[] {
|
|
712
|
+
return this.db.prepare(`
|
|
713
|
+
SELECT snapshotId, projectId, fileId, path, contentHash
|
|
714
|
+
FROM code_state_snapshot_files
|
|
715
|
+
WHERE snapshotId = ?
|
|
716
|
+
ORDER BY path
|
|
717
|
+
`).all(snapshotId).map(rowToSnapshotFile);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
diffSnapshots(projectId: string, fromSnapshotId: string, toSnapshotId: string): CodeStateDiff {
|
|
721
|
+
const snapshots = this.db.prepare(`
|
|
722
|
+
SELECT * FROM code_state_snapshots WHERE id IN (?, ?)
|
|
723
|
+
`).all(fromSnapshotId, toSnapshotId).map(rowToSnapshot) as CodeStateSnapshot[];
|
|
724
|
+
if (snapshots.length !== 2) {
|
|
725
|
+
return { projectId, fromSnapshotId, toSnapshotId, available: false, reason: 'missing-snapshot', changes: [] };
|
|
726
|
+
}
|
|
727
|
+
if (snapshots.some(snapshot => snapshot.projectId !== projectId)) {
|
|
728
|
+
return { projectId, fromSnapshotId, toSnapshotId, available: false, reason: 'project-mismatch', changes: [] };
|
|
729
|
+
}
|
|
730
|
+
if (snapshots.some(snapshotIsIncomplete)) {
|
|
731
|
+
return { projectId, fromSnapshotId, toSnapshotId, available: false, reason: 'incomplete-snapshot', changes: [] };
|
|
732
|
+
}
|
|
733
|
+
const before = this.listSnapshotFiles(fromSnapshotId);
|
|
734
|
+
const after = this.listSnapshotFiles(toSnapshotId);
|
|
735
|
+
if (before.length === 0 || after.length === 0) {
|
|
736
|
+
return {
|
|
737
|
+
projectId,
|
|
738
|
+
fromSnapshotId,
|
|
739
|
+
toSnapshotId,
|
|
740
|
+
available: false,
|
|
741
|
+
reason: 'legacy-snapshot-without-manifest',
|
|
742
|
+
changes: [],
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
const beforeByPath = new Map(before.map(file => [file.path, file]));
|
|
746
|
+
const afterByPath = new Map(after.map(file => [file.path, file]));
|
|
747
|
+
const paths = [...new Set([...beforeByPath.keys(), ...afterByPath.keys()])].sort();
|
|
748
|
+
const changes: CodeStateFileChange[] = [];
|
|
749
|
+
for (const path of paths) {
|
|
750
|
+
const oldFile = beforeByPath.get(path);
|
|
751
|
+
const newFile = afterByPath.get(path);
|
|
752
|
+
if (!oldFile && newFile) {
|
|
753
|
+
changes.push({ path, kind: 'added', afterHash: newFile.contentHash });
|
|
754
|
+
} else if (oldFile && !newFile) {
|
|
755
|
+
changes.push({ path, kind: 'removed', beforeHash: oldFile.contentHash });
|
|
756
|
+
} else if (oldFile && newFile && oldFile.contentHash !== newFile.contentHash) {
|
|
757
|
+
changes.push({ path, kind: 'modified', beforeHash: oldFile.contentHash, afterHash: newFile.contentHash });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return { projectId, fromSnapshotId, toSnapshotId, available: true, changes };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
impactSlice(projectId: string, changedPaths: string[], maxRelations = 24): CodeGraphImpactSlice {
|
|
764
|
+
const safeLimit = Number.isFinite(maxRelations) ? Math.max(1, Math.min(100, Math.floor(maxRelations))) : 24;
|
|
765
|
+
const normalizedPaths = [...new Set(changedPaths.map(normalizeCodePath).filter(Boolean))].sort();
|
|
766
|
+
const files = this.listFiles(projectId);
|
|
767
|
+
const fileById = new Map(files.map(file => [file.id, file.path]));
|
|
768
|
+
const fileIdBySymbolId = new Map(this.listSymbols(projectId).map(symbol => [symbol.id, symbol.fileId]));
|
|
769
|
+
const changedIds = new Set(files.filter(file => normalizedPaths.includes(file.path)).map(file => file.id));
|
|
770
|
+
const direct = new Set<string>();
|
|
771
|
+
let relationCount = 0;
|
|
772
|
+
let truncated = false;
|
|
773
|
+
for (const edge of this.listEdges(projectId)) {
|
|
774
|
+
const endpoints = [
|
|
775
|
+
edge.fromFileId,
|
|
776
|
+
edge.toFileId,
|
|
777
|
+
edge.fromSymbolId ? fileIdBySymbolId.get(edge.fromSymbolId) : undefined,
|
|
778
|
+
edge.toSymbolId ? fileIdBySymbolId.get(edge.toSymbolId) : undefined,
|
|
779
|
+
].filter((id): id is string => Boolean(id));
|
|
780
|
+
if (!endpoints.some(id => changedIds.has(id))) continue;
|
|
781
|
+
const relatedPaths = endpoints
|
|
782
|
+
.filter(id => !changedIds.has(id))
|
|
783
|
+
.map(id => fileById.get(id))
|
|
784
|
+
.filter((path): path is string => Boolean(path));
|
|
785
|
+
if (relatedPaths.length === 0) continue;
|
|
786
|
+
if (relationCount >= safeLimit) {
|
|
787
|
+
truncated = true;
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
relationCount += 1;
|
|
791
|
+
for (const path of relatedPaths) direct.add(path);
|
|
792
|
+
}
|
|
793
|
+
const snapshot = this.latestSnapshot(projectId);
|
|
794
|
+
return {
|
|
795
|
+
projectId,
|
|
796
|
+
...(snapshot ? { snapshotId: snapshot.id } : {}),
|
|
797
|
+
changedPaths: normalizedPaths,
|
|
798
|
+
directlyConnectedPaths: [...direct].sort(),
|
|
799
|
+
relationCount,
|
|
800
|
+
truncated,
|
|
801
|
+
provider: snapshot?.provider ?? 'lite',
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
604
805
|
/**
|
|
605
806
|
* Record a completed scan and mark all current structural facts with its
|
|
606
807
|
* epoch. The snapshot is written only after refresh reconciliation succeeds,
|
|
@@ -642,6 +843,16 @@ export class CodeGraphStore {
|
|
|
642
843
|
this.db.prepare(
|
|
643
844
|
"UPDATE observation_code_refs SET snapshotId = ? WHERE projectId = ? AND status = 'current'",
|
|
644
845
|
).run(id, input.projectId);
|
|
846
|
+
const snapshotFileRows = this.db.prepare(`
|
|
847
|
+
SELECT id, projectId, path, contentHash FROM code_files WHERE projectId = ? ORDER BY path
|
|
848
|
+
`).all(input.projectId) as Array<{ id: string; projectId: string; path: string; contentHash: string }>;
|
|
849
|
+
const insertSnapshotFile = this.db.prepare(`
|
|
850
|
+
INSERT INTO code_state_snapshot_files (snapshotId, projectId, fileId, path, contentHash)
|
|
851
|
+
VALUES (?, ?, ?, ?, ?)
|
|
852
|
+
`);
|
|
853
|
+
for (const file of snapshotFileRows) {
|
|
854
|
+
insertSnapshotFile.run(id, file.projectId, file.id, file.path, file.contentHash);
|
|
855
|
+
}
|
|
645
856
|
snapshot = {
|
|
646
857
|
...input,
|
|
647
858
|
id,
|
package/src/codegraph/types.ts
CHANGED
|
@@ -116,6 +116,46 @@ export interface CodeStateSnapshot extends CodeStateSnapshotInput {
|
|
|
116
116
|
previousSnapshotId?: string;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** Hash-only file state retained for an explicit Code State snapshot. */
|
|
120
|
+
export interface CodeStateSnapshotFile {
|
|
121
|
+
snapshotId: string;
|
|
122
|
+
projectId: string;
|
|
123
|
+
fileId: string;
|
|
124
|
+
path: string;
|
|
125
|
+
contentHash: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface CodeStateFileChange {
|
|
129
|
+
path: string;
|
|
130
|
+
kind: 'added' | 'modified' | 'removed';
|
|
131
|
+
beforeHash?: string;
|
|
132
|
+
afterHash?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* An exact file-level comparison when both snapshots have hash manifests.
|
|
137
|
+
* It intentionally makes no claim about source-level semantic equivalence.
|
|
138
|
+
*/
|
|
139
|
+
export interface CodeStateDiff {
|
|
140
|
+
projectId: string;
|
|
141
|
+
fromSnapshotId: string;
|
|
142
|
+
toSnapshotId: string;
|
|
143
|
+
available: boolean;
|
|
144
|
+
reason?: 'missing-snapshot' | 'project-mismatch' | 'legacy-snapshot-without-manifest' | 'incomplete-snapshot';
|
|
145
|
+
changes: CodeStateFileChange[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Bounded one-hop structural evidence around changed file paths. */
|
|
149
|
+
export interface CodeGraphImpactSlice {
|
|
150
|
+
projectId: string;
|
|
151
|
+
snapshotId?: string;
|
|
152
|
+
changedPaths: string[];
|
|
153
|
+
directlyConnectedPaths: string[];
|
|
154
|
+
relationCount: number;
|
|
155
|
+
truncated: boolean;
|
|
156
|
+
provider: CodeGraphProviderKind;
|
|
157
|
+
}
|
|
158
|
+
|
|
119
159
|
export interface CodeGraphStatus {
|
|
120
160
|
provider: CodeGraphProviderKind;
|
|
121
161
|
files: number;
|
|
@@ -164,6 +204,8 @@ export interface CodeGraphProviderQuality {
|
|
|
164
204
|
capabilities: {
|
|
165
205
|
declarations: boolean;
|
|
166
206
|
importHints: boolean;
|
|
207
|
+
/** Deterministic local-path mapping only; not semantic name resolution. */
|
|
208
|
+
localRelativeImportTargets: boolean;
|
|
167
209
|
resolvedRelations: false;
|
|
168
210
|
exactLocations: false;
|
|
169
211
|
};
|