memorix 1.8.6 → 1.8.7

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 (49) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/dist/cli/index.js +2208 -1160
  5. package/dist/dashboard/static/app.js +51 -3
  6. package/dist/dashboard/static/style.css +88 -0
  7. package/dist/index.js +1726 -736
  8. package/dist/maintenance-runner.js +855 -218
  9. package/dist/media-audio-runner.js +856 -219
  10. package/dist/media-video-runner.js +854 -217
  11. package/dist/memcode-runtime/CHANGELOG.md +33 -0
  12. package/dist/sdk.js +1716 -726
  13. package/dist/vector-backfill-runner.js +853 -216
  14. package/docs/1.8.7-CODEGRAPH.md +166 -0
  15. package/docs/API_REFERENCE.md +23 -2
  16. package/docs/ARCHITECTURE.md +13 -0
  17. package/docs/README.md +4 -1
  18. package/package.json +3 -2
  19. package/plugins/antigravity/memorix/plugin.json +1 -1
  20. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  21. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  22. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  23. package/plugins/copilot/memorix/plugin.json +1 -1
  24. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  25. package/plugins/hermes/memorix/plugin.yaml +1 -1
  26. package/plugins/omp/memorix/package.json +1 -1
  27. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  28. package/plugins/pi/memorix/package.json +1 -1
  29. package/server.json +2 -2
  30. package/src/cli/commands/codegraph.ts +25 -11
  31. package/src/cli/commands/doctor.ts +11 -2
  32. package/src/cli/commands/serve-http.ts +27 -1
  33. package/src/codegraph/auto-context.ts +19 -10
  34. package/src/codegraph/context-pack.ts +20 -6
  35. package/src/codegraph/dashboard.ts +82 -0
  36. package/src/codegraph/external-provider.ts +10 -1
  37. package/src/codegraph/freshness.ts +1 -1
  38. package/src/codegraph/lite-provider.ts +81 -0
  39. package/src/codegraph/project-context.ts +18 -5
  40. package/src/codegraph/scip-provider.ts +181 -0
  41. package/src/codegraph/semantic-context.ts +139 -0
  42. package/src/codegraph/semantic-provider.ts +404 -0
  43. package/src/codegraph/store.ts +253 -18
  44. package/src/codegraph/types.ts +25 -3
  45. package/src/dashboard/server.ts +6 -0
  46. package/src/dashboard/static/style.css +88 -0
  47. package/src/knowledge/workset.ts +46 -44
  48. package/src/server/mcp-binding-store.ts +17 -0
  49. package/src/store/sqlite-db.ts +13 -0
@@ -34,6 +34,8 @@ function rowToFile(row: any): CodeFile {
34
34
  ...(row.sizeBytes != null ? { sizeBytes: row.sizeBytes } : {}),
35
35
  indexedAt: row.indexedAt,
36
36
  ...(row.gitCommit ? { gitCommit: row.gitCommit } : {}),
37
+ ...(row.source ? { source: row.source } : {}),
38
+ ...(row.parserError ? { parserError: row.parserError } : {}),
37
39
  ...(row.snapshotId ? { snapshotId: row.snapshotId } : {}),
38
40
  ...(row.sourceEpoch != null ? { sourceEpoch: Number(row.sourceEpoch) } : {}),
39
41
  };
@@ -54,6 +56,7 @@ function rowToSymbol(row: any): CodeSymbol {
54
56
  ...(row.contentHash ? { contentHash: row.contentHash } : {}),
55
57
  indexedAt: row.indexedAt,
56
58
  stale: !!row.stale,
59
+ ...(row.source ? { source: row.source } : {}),
57
60
  ...(row.snapshotId ? { snapshotId: row.snapshotId } : {}),
58
61
  ...(row.sourceEpoch != null ? { sourceEpoch: Number(row.sourceEpoch) } : {}),
59
62
  } as CodeSymbol;
@@ -70,6 +73,7 @@ function rowToEdge(row: any): CodeEdge {
70
73
  type: row.type,
71
74
  confidence: row.confidence,
72
75
  ...(row.evidence ? { evidence: row.evidence } : {}),
76
+ ...(row.source ? { source: row.source } : {}),
73
77
  indexedAt: row.indexedAt,
74
78
  ...(row.snapshotId ? { snapshotId: row.snapshotId } : {}),
75
79
  ...(row.sourceEpoch != null ? { sourceEpoch: Number(row.sourceEpoch) } : {}),
@@ -209,9 +213,9 @@ export class CodeGraphStore {
209
213
  if (files.length === 0) return;
210
214
  const stmt = this.db.prepare(`
211
215
  INSERT OR REPLACE INTO code_files
212
- (id, projectId, path, language, contentHash, mtimeMs, sizeBytes, indexedAt, gitCommit)
216
+ (id, projectId, path, language, contentHash, mtimeMs, sizeBytes, indexedAt, gitCommit, source, parserError)
213
217
  VALUES
214
- (@id, @projectId, @path, @language, @contentHash, @mtimeMs, @sizeBytes, @indexedAt, @gitCommit)
218
+ (@id, @projectId, @path, @language, @contentHash, @mtimeMs, @sizeBytes, @indexedAt, @gitCommit, @source, @parserError)
215
219
  `);
216
220
  const tx = this.db.transaction((items: CodeFile[]) => {
217
221
  for (const file of items) {
@@ -225,6 +229,8 @@ export class CodeGraphStore {
225
229
  sizeBytes: file.sizeBytes ?? null,
226
230
  indexedAt: file.indexedAt,
227
231
  gitCommit: file.gitCommit ?? null,
232
+ source: file.source ?? null,
233
+ parserError: file.parserError ?? null,
228
234
  });
229
235
  }
230
236
  });
@@ -235,9 +241,9 @@ export class CodeGraphStore {
235
241
  if (symbols.length === 0) return;
236
242
  const stmt = this.db.prepare(`
237
243
  INSERT OR REPLACE INTO code_symbols
238
- (id, projectId, fileId, path, name, qualifiedName, kind, startLine, endLine, signature, contentHash, indexedAt, stale)
244
+ (id, projectId, fileId, path, name, qualifiedName, kind, startLine, endLine, signature, contentHash, indexedAt, stale, source)
239
245
  VALUES
240
- (@id, @projectId, @fileId, @path, @name, @qualifiedName, @kind, @startLine, @endLine, @signature, @contentHash, @indexedAt, @stale)
246
+ (@id, @projectId, @fileId, @path, @name, @qualifiedName, @kind, @startLine, @endLine, @signature, @contentHash, @indexedAt, @stale, @source)
241
247
  `);
242
248
  const tx = this.db.transaction((items: CodeSymbol[]) => {
243
249
  for (const symbol of items) {
@@ -255,6 +261,7 @@ export class CodeGraphStore {
255
261
  contentHash: symbol.contentHash ?? null,
256
262
  indexedAt: symbol.indexedAt,
257
263
  stale: symbol.stale ? 1 : 0,
264
+ source: symbol.source ?? null,
258
265
  });
259
266
  }
260
267
  });
@@ -265,9 +272,9 @@ export class CodeGraphStore {
265
272
  if (edges.length === 0) return;
266
273
  const stmt = this.db.prepare(`
267
274
  INSERT OR REPLACE INTO code_edges
268
- (id, projectId, fromSymbolId, toSymbolId, fromFileId, toFileId, type, confidence, evidence, indexedAt)
275
+ (id, projectId, fromSymbolId, toSymbolId, fromFileId, toFileId, type, confidence, evidence, indexedAt, source)
269
276
  VALUES
270
- (@id, @projectId, @fromSymbolId, @toSymbolId, @fromFileId, @toFileId, @type, @confidence, @evidence, @indexedAt)
277
+ (@id, @projectId, @fromSymbolId, @toSymbolId, @fromFileId, @toFileId, @type, @confidence, @evidence, @indexedAt, @source)
271
278
  `);
272
279
  const tx = this.db.transaction((items: CodeEdge[]) => {
273
280
  for (const edge of items) {
@@ -282,6 +289,7 @@ export class CodeGraphStore {
282
289
  confidence: edge.confidence,
283
290
  evidence: edge.evidence ?? null,
284
291
  indexedAt: edge.indexedAt,
292
+ source: edge.source ?? null,
285
293
  });
286
294
  }
287
295
  });
@@ -297,21 +305,21 @@ export class CodeGraphStore {
297
305
  const deleteFiles = this.db.prepare(`DELETE FROM code_files WHERE projectId = ?`);
298
306
  const insertFile = this.db.prepare(`
299
307
  INSERT OR REPLACE INTO code_files
300
- (id, projectId, path, language, contentHash, mtimeMs, sizeBytes, indexedAt, gitCommit)
308
+ (id, projectId, path, language, contentHash, mtimeMs, sizeBytes, indexedAt, gitCommit, source, parserError)
301
309
  VALUES
302
- (@id, @projectId, @path, @language, @contentHash, @mtimeMs, @sizeBytes, @indexedAt, @gitCommit)
310
+ (@id, @projectId, @path, @language, @contentHash, @mtimeMs, @sizeBytes, @indexedAt, @gitCommit, @source, @parserError)
303
311
  `);
304
312
  const insertSymbol = this.db.prepare(`
305
313
  INSERT OR REPLACE INTO code_symbols
306
- (id, projectId, fileId, path, name, qualifiedName, kind, startLine, endLine, signature, contentHash, indexedAt, stale)
314
+ (id, projectId, fileId, path, name, qualifiedName, kind, startLine, endLine, signature, contentHash, indexedAt, stale, source)
307
315
  VALUES
308
- (@id, @projectId, @fileId, @path, @name, @qualifiedName, @kind, @startLine, @endLine, @signature, @contentHash, @indexedAt, @stale)
316
+ (@id, @projectId, @fileId, @path, @name, @qualifiedName, @kind, @startLine, @endLine, @signature, @contentHash, @indexedAt, @stale, @source)
309
317
  `);
310
318
  const insertEdge = this.db.prepare(`
311
319
  INSERT OR REPLACE INTO code_edges
312
- (id, projectId, fromSymbolId, toSymbolId, fromFileId, toFileId, type, confidence, evidence, indexedAt)
320
+ (id, projectId, fromSymbolId, toSymbolId, fromFileId, toFileId, type, confidence, evidence, indexedAt, source)
313
321
  VALUES
314
- (@id, @projectId, @fromSymbolId, @toSymbolId, @fromFileId, @toFileId, @type, @confidence, @evidence, @indexedAt)
322
+ (@id, @projectId, @fromSymbolId, @toSymbolId, @fromFileId, @toFileId, @type, @confidence, @evidence, @indexedAt, @source)
315
323
  `);
316
324
 
317
325
  const tx = this.db.transaction(() => {
@@ -330,6 +338,8 @@ export class CodeGraphStore {
330
338
  sizeBytes: file.sizeBytes ?? null,
331
339
  indexedAt: file.indexedAt,
332
340
  gitCommit: file.gitCommit ?? null,
341
+ source: file.source ?? null,
342
+ parserError: file.parserError ?? null,
333
343
  });
334
344
  }
335
345
 
@@ -348,6 +358,7 @@ export class CodeGraphStore {
348
358
  contentHash: symbol.contentHash ?? null,
349
359
  indexedAt: symbol.indexedAt,
350
360
  stale: 0,
361
+ source: symbol.source ?? null,
351
362
  });
352
363
  }
353
364
 
@@ -363,6 +374,7 @@ export class CodeGraphStore {
363
374
  confidence: edge.confidence,
364
375
  evidence: edge.evidence ?? null,
365
376
  indexedAt: edge.indexedAt,
377
+ source: edge.source ?? null,
366
378
  });
367
379
  }
368
380
  });
@@ -417,21 +429,21 @@ export class CodeGraphStore {
417
429
  const deleteFile = this.db.prepare(`DELETE FROM code_files WHERE projectId = ? AND id = ?`);
418
430
  const upsertFile = this.db.prepare(`
419
431
  INSERT OR REPLACE INTO code_files
420
- (id, projectId, path, language, contentHash, mtimeMs, sizeBytes, indexedAt, gitCommit)
432
+ (id, projectId, path, language, contentHash, mtimeMs, sizeBytes, indexedAt, gitCommit, source, parserError)
421
433
  VALUES
422
- (@id, @projectId, @path, @language, @contentHash, @mtimeMs, @sizeBytes, @indexedAt, @gitCommit)
434
+ (@id, @projectId, @path, @language, @contentHash, @mtimeMs, @sizeBytes, @indexedAt, @gitCommit, @source, @parserError)
423
435
  `);
424
436
  const upsertSymbol = this.db.prepare(`
425
437
  INSERT OR REPLACE INTO code_symbols
426
- (id, projectId, fileId, path, name, qualifiedName, kind, startLine, endLine, signature, contentHash, indexedAt, stale)
438
+ (id, projectId, fileId, path, name, qualifiedName, kind, startLine, endLine, signature, contentHash, indexedAt, stale, source)
427
439
  VALUES
428
- (@id, @projectId, @fileId, @path, @name, @qualifiedName, @kind, @startLine, @endLine, @signature, @contentHash, @indexedAt, @stale)
440
+ (@id, @projectId, @fileId, @path, @name, @qualifiedName, @kind, @startLine, @endLine, @signature, @contentHash, @indexedAt, @stale, @source)
429
441
  `);
430
442
  const upsertEdge = this.db.prepare(`
431
443
  INSERT OR REPLACE INTO code_edges
432
- (id, projectId, fromSymbolId, toSymbolId, fromFileId, toFileId, type, confidence, evidence, indexedAt)
444
+ (id, projectId, fromSymbolId, toSymbolId, fromFileId, toFileId, type, confidence, evidence, indexedAt, source)
433
445
  VALUES
434
- (@id, @projectId, @fromSymbolId, @toSymbolId, @fromFileId, @toFileId, @type, @confidence, @evidence, @indexedAt)
446
+ (@id, @projectId, @fromSymbolId, @toSymbolId, @fromFileId, @toFileId, @type, @confidence, @evidence, @indexedAt, @source)
435
447
  `);
436
448
  const writeFile = (file: CodeFile) => upsertFile.run({
437
449
  id: file.id,
@@ -443,6 +455,8 @@ export class CodeGraphStore {
443
455
  sizeBytes: file.sizeBytes ?? null,
444
456
  indexedAt: file.indexedAt,
445
457
  gitCommit: file.gitCommit ?? null,
458
+ source: file.source ?? null,
459
+ parserError: file.parserError ?? null,
446
460
  });
447
461
 
448
462
  const tx = this.db.transaction(() => {
@@ -480,6 +494,7 @@ export class CodeGraphStore {
480
494
  contentHash: symbol.contentHash ?? null,
481
495
  indexedAt: symbol.indexedAt,
482
496
  stale: symbol.stale ? 1 : 0,
497
+ source: symbol.source ?? null,
483
498
  });
484
499
  }
485
500
 
@@ -495,6 +510,7 @@ export class CodeGraphStore {
495
510
  confidence: edge.confidence,
496
511
  evidence: edge.evidence ?? null,
497
512
  indexedAt: edge.indexedAt,
513
+ source: edge.source ?? null,
498
514
  });
499
515
  }
500
516
  }
@@ -655,6 +671,219 @@ export class CodeGraphStore {
655
671
  return this.db.prepare(`SELECT * FROM code_edges WHERE projectId = ? ORDER BY type, id`).all(projectId).map(rowToEdge);
656
672
  }
657
673
 
674
+ /** Return direct importers before a refresh clears their old target IDs. */
675
+ listImportDependentPaths(projectId: string, paths: string[], candidatePaths: string[] = []): string[] {
676
+ const normalized = [...new Set(paths.map(normalizeCodePath).filter(Boolean))];
677
+ if (normalized.length === 0) return [];
678
+ const files = this.listFiles(projectId);
679
+ const fileIds = files
680
+ .filter(file => normalized.includes(file.path))
681
+ .map(file => file.id);
682
+ const fileById = new Map(files.map(file => [file.id, file]));
683
+ const fileByPath = new Map(files.map(file => [file.path, file]));
684
+ for (const path of candidatePaths.map(normalizeCodePath).filter(Boolean)) {
685
+ if (!fileByPath.has(path)) {
686
+ fileByPath.set(path, {
687
+ id: `candidate:${path}`,
688
+ projectId,
689
+ path,
690
+ contentHash: '',
691
+ indexedAt: '',
692
+ });
693
+ }
694
+ }
695
+ const rows = this.db.prepare(`
696
+ SELECT DISTINCT fromFileId, evidence
697
+ FROM code_edges
698
+ WHERE projectId = ? AND type = 'imports'
699
+ `).all(projectId) as Array<{ fromFileId?: string; evidence?: string }>;
700
+ const wanted = new Set(normalized);
701
+ const dependents = new Set<string>();
702
+ const queue = [...normalized];
703
+ while (queue.length > 0) {
704
+ const targetPath = queue.shift()!;
705
+ for (const row of rows) {
706
+ const source = row.fromFileId ? fileById.get(row.fromFileId) : undefined;
707
+ if (!source || source.path === targetPath || dependents.has(source.path)) continue;
708
+ const target = resolveRelativeImportTarget(source.path, row.evidence, fileByPath);
709
+ if (!target || !wanted.has(target.path)) continue;
710
+ dependents.add(source.path);
711
+ if (!wanted.has(source.path)) {
712
+ wanted.add(source.path);
713
+ queue.push(source.path);
714
+ }
715
+ }
716
+ }
717
+ if (fileIds.length > 0) {
718
+ const placeholders = fileIds.map(() => '?').join(', ');
719
+ const exactRows = this.db.prepare(`
720
+ SELECT DISTINCT fromFileId
721
+ FROM code_edges
722
+ WHERE projectId = ? AND type = 'imports' AND toFileId IN (${placeholders})
723
+ `).all(projectId, ...fileIds) as Array<{ fromFileId?: string }>;
724
+ for (const row of exactRows) {
725
+ const source = row.fromFileId ? fileById.get(row.fromFileId) : undefined;
726
+ if (source) dependents.add(source.path);
727
+ }
728
+ }
729
+ return [...dependents].sort();
730
+ }
731
+
732
+ /** Replace only the semantic slice touched by a change, in one transaction. */
733
+ replaceSemanticSlice(
734
+ projectId: string,
735
+ affectedPaths: string[],
736
+ input: { symbols: CodeSymbol[]; edges: CodeEdge[]; diagnostics?: Array<{ path: string; message: string }> },
737
+ ): void {
738
+ const normalizedPaths = [...new Set(affectedPaths.map(normalizeCodePath).filter(Boolean))];
739
+ if (normalizedPaths.length === 0) return;
740
+ const fileRows = this.listFiles(projectId).filter(file => normalizedPaths.includes(file.path));
741
+ const fileIds = fileRows.map(file => file.id);
742
+ const placeholders = fileIds.map(() => '?').join(', ');
743
+ if (fileIds.length === 0) return;
744
+ const staleRefs = this.db.prepare(`
745
+ UPDATE observation_code_refs SET status = 'stale', updatedAt = ?
746
+ WHERE projectId = ? AND (fileId IN (${placeholders}) OR symbolId IN (
747
+ SELECT id FROM code_symbols WHERE projectId = ? AND fileId IN (${placeholders})
748
+ ))
749
+ `);
750
+ const deleteEdges = this.db.prepare(`
751
+ DELETE FROM code_edges
752
+ WHERE projectId = ? AND (source IS NULL OR source IN ('lite-regex', 'typescript-compiler'))
753
+ AND (fromFileId IN (${placeholders}) OR toFileId IN (${placeholders})
754
+ OR fromSymbolId IN (SELECT id FROM code_symbols WHERE projectId = ? AND fileId IN (${placeholders}))
755
+ OR toSymbolId IN (SELECT id FROM code_symbols WHERE projectId = ? AND fileId IN (${placeholders})))
756
+ `);
757
+ const deleteSymbols = this.db.prepare(`DELETE FROM code_symbols WHERE projectId = ? AND fileId IN (${placeholders})`);
758
+ const updateFile = this.db.prepare('UPDATE code_files SET source = ?, parserError = ? WHERE projectId = ? AND id = ?');
759
+ const insertSymbol = this.db.prepare(`
760
+ INSERT OR REPLACE INTO code_symbols
761
+ (id, projectId, fileId, path, name, qualifiedName, kind, startLine, endLine, signature, contentHash, indexedAt, stale, source)
762
+ VALUES
763
+ (@id, @projectId, @fileId, @path, @name, @qualifiedName, @kind, @startLine, @endLine, @signature, @contentHash, @indexedAt, @stale, @source)
764
+ `);
765
+ const insertEdge = this.db.prepare(`
766
+ INSERT OR REPLACE INTO code_edges
767
+ (id, projectId, fromSymbolId, toSymbolId, fromFileId, toFileId, type, confidence, evidence, indexedAt, source)
768
+ VALUES
769
+ (@id, @projectId, @fromSymbolId, @toSymbolId, @fromFileId, @toFileId, @type, @confidence, @evidence, @indexedAt, @source)
770
+ `);
771
+ const diagnosticsByPath = new Map((input.diagnostics ?? []).map(item => [normalizeCodePath(item.path), item.message]));
772
+ const symbolFileIds = new Set(fileIds);
773
+ const tx = this.db.transaction(() => {
774
+ const now = new Date().toISOString();
775
+ staleRefs.run(now, projectId, ...fileIds, projectId, ...fileIds);
776
+ deleteEdges.run(projectId, ...fileIds, ...fileIds, projectId, ...fileIds, projectId, ...fileIds);
777
+ deleteSymbols.run(projectId, ...fileIds);
778
+ for (const file of fileRows) {
779
+ const diagnostic = diagnosticsByPath.get(file.path);
780
+ updateFile.run(diagnostic ? 'typescript-compiler' : 'typescript-compiler', diagnostic ?? null, projectId, file.id);
781
+ }
782
+ for (const symbol of input.symbols.filter(symbol => symbolFileIds.has(symbol.fileId))) {
783
+ insertSymbol.run({
784
+ id: symbol.id,
785
+ projectId: symbol.projectId,
786
+ fileId: symbol.fileId,
787
+ path: normalizeCodePath(symbol.path),
788
+ name: symbol.name,
789
+ qualifiedName: symbol.qualifiedName,
790
+ kind: symbol.kind,
791
+ startLine: symbol.startLine ?? null,
792
+ endLine: symbol.endLine ?? null,
793
+ signature: symbol.signature ?? null,
794
+ contentHash: symbol.contentHash ?? null,
795
+ indexedAt: symbol.indexedAt,
796
+ stale: symbol.stale ? 1 : 0,
797
+ source: symbol.source ?? 'typescript-compiler',
798
+ });
799
+ }
800
+ for (const edge of input.edges) {
801
+ insertEdge.run({
802
+ id: edge.id,
803
+ projectId: edge.projectId,
804
+ fromSymbolId: edge.fromSymbolId ?? null,
805
+ toSymbolId: edge.toSymbolId ?? null,
806
+ fromFileId: edge.fromFileId ?? null,
807
+ toFileId: edge.toFileId ?? null,
808
+ type: edge.type,
809
+ confidence: edge.confidence,
810
+ evidence: edge.evidence ?? null,
811
+ indexedAt: edge.indexedAt,
812
+ source: edge.source ?? 'typescript-compiler',
813
+ });
814
+ }
815
+ });
816
+ tx();
817
+ }
818
+
819
+ /** Record a semantic refresh failure without presenting the old facts as current. */
820
+ markSemanticSliceFailed(projectId: string, affectedPaths: string[], message: string): void {
821
+ const normalizedPaths = [...new Set(affectedPaths.map(normalizeCodePath).filter(Boolean))];
822
+ const fileRows = this.listFiles(projectId).filter(file => normalizedPaths.includes(file.path));
823
+ const fileIds = fileRows.map(file => file.id);
824
+ const filePlaceholders = fileIds.map(() => '?').join(', ');
825
+ const staleRefs = fileIds.length > 0
826
+ ? this.db.prepare(`
827
+ UPDATE observation_code_refs SET status = 'stale', updatedAt = ?
828
+ WHERE projectId = ? AND (fileId IN (${filePlaceholders}) OR symbolId IN (
829
+ SELECT id FROM code_symbols WHERE projectId = ? AND fileId IN (${filePlaceholders})
830
+ ))
831
+ `)
832
+ : undefined;
833
+ const staleSymbols = fileIds.length > 0
834
+ ? this.db.prepare(`
835
+ UPDATE code_symbols SET stale = 1
836
+ WHERE projectId = ? AND source = 'typescript-compiler' AND fileId IN (${filePlaceholders})
837
+ `)
838
+ : undefined;
839
+ const updateFiles = fileIds.length > 0
840
+ ? this.db.prepare(`
841
+ UPDATE code_files SET parserError = ?
842
+ WHERE projectId = ? AND id IN (${filePlaceholders})
843
+ `)
844
+ : undefined;
845
+ const affectedEdgeClause = fileIds.length > 0
846
+ ? `
847
+ OR fromFileId IN (${filePlaceholders})
848
+ OR toFileId IN (${filePlaceholders})
849
+ OR fromSymbolId IN (SELECT id FROM code_symbols WHERE projectId = ? AND fileId IN (${filePlaceholders}))
850
+ OR toSymbolId IN (SELECT id FROM code_symbols WHERE projectId = ? AND fileId IN (${filePlaceholders}))
851
+ `
852
+ : '';
853
+ const deleteEdges = this.db.prepare(`
854
+ DELETE FROM code_edges
855
+ WHERE projectId = ? AND source = 'typescript-compiler'
856
+ AND (
857
+ (fromSymbolId IS NOT NULL AND NOT EXISTS (
858
+ SELECT 1 FROM code_symbols WHERE projectId = ? AND id = fromSymbolId
859
+ ))
860
+ OR (toSymbolId IS NOT NULL AND NOT EXISTS (
861
+ SELECT 1 FROM code_symbols WHERE projectId = ? AND id = toSymbolId
862
+ ))
863
+ ${affectedEdgeClause}
864
+ )
865
+ `);
866
+ const tx = this.db.transaction(() => {
867
+ const failure = `semantic indexing failed: ${message.slice(0, 500)}`;
868
+ if (updateFiles) updateFiles.run(failure, projectId, ...fileIds);
869
+ if (staleRefs) staleRefs.run(new Date().toISOString(), projectId, ...fileIds, projectId, ...fileIds);
870
+ if (staleSymbols) staleSymbols.run(projectId, ...fileIds);
871
+ const edgeParams: unknown[] = [projectId, projectId, projectId];
872
+ if (fileIds.length > 0) {
873
+ edgeParams.push(
874
+ ...fileIds,
875
+ ...fileIds,
876
+ projectId,
877
+ ...fileIds,
878
+ projectId,
879
+ ...fileIds,
880
+ );
881
+ }
882
+ deleteEdges.run(...edgeParams);
883
+ });
884
+ tx();
885
+ }
886
+
658
887
  listObservationRefs(projectId: string, observationId: number): ObservationCodeRef[] {
659
888
  return this.db.prepare(`
660
889
  SELECT * FROM observation_code_refs
@@ -869,6 +1098,9 @@ export class CodeGraphStore {
869
1098
  const symbols = this.db.prepare(`SELECT COUNT(*) AS count FROM code_symbols WHERE projectId = ? AND stale = 0`).get(projectId).count;
870
1099
  const edges = this.db.prepare(`SELECT COUNT(*) AS count FROM code_edges WHERE projectId = ?`).get(projectId).count;
871
1100
  const refs = this.db.prepare(`SELECT COUNT(*) AS count FROM observation_code_refs WHERE projectId = ?`).get(projectId).count;
1101
+ const semanticSymbols = this.db.prepare(`SELECT COUNT(*) AS count FROM code_symbols WHERE projectId = ? AND stale = 0 AND source = 'typescript-compiler'`).get(projectId).count;
1102
+ const semanticEdges = this.db.prepare(`SELECT COUNT(*) AS count FROM code_edges WHERE projectId = ? AND source = 'typescript-compiler'`).get(projectId).count;
1103
+ const parserErrors = this.db.prepare(`SELECT COUNT(*) AS count FROM code_files WHERE projectId = ? AND parserError IS NOT NULL`).get(projectId).count;
872
1104
  const latest = this.db.prepare(`SELECT MAX(indexedAt) AS indexedAt FROM code_files WHERE projectId = ?`).get(projectId);
873
1105
  const latestSnapshot = this.latestSnapshot(projectId);
874
1106
  return {
@@ -877,6 +1109,9 @@ export class CodeGraphStore {
877
1109
  symbols,
878
1110
  edges,
879
1111
  refs,
1112
+ semanticSymbols,
1113
+ semanticEdges,
1114
+ parserErrors,
880
1115
  ...(latest?.indexedAt ? { indexedAt: latest.indexedAt } : {}),
881
1116
  ...(latestSnapshot ? { latestSnapshot } : {}),
882
1117
  };
@@ -1,4 +1,8 @@
1
1
  export type CodeGraphProviderKind = 'external' | 'lite';
2
+ export type CodeGraphEvidenceProvider = 'lite' | 'semantic' | 'external';
3
+
4
+ /** The component that produced a persisted code fact. */
5
+ export type CodeEvidenceSource = 'lite-regex' | 'typescript-compiler' | 'scip' | 'external';
2
6
 
3
7
  export type CodeRefStatus = 'current' | 'suspect' | 'stale' | 'unbound';
4
8
 
@@ -10,6 +14,7 @@ export type CodeSymbolKind =
10
14
  | 'type'
11
15
  | 'component'
12
16
  | 'constant'
17
+ | 'property'
13
18
  | 'route'
14
19
  | 'unknown';
15
20
 
@@ -19,6 +24,8 @@ export type CodeEdgeType =
19
24
  | 'calls'
20
25
  | 'defines'
21
26
  | 'tests'
27
+ | 'extends'
28
+ | 'implements'
22
29
  | 'routes_to'
23
30
  | 'references';
24
31
 
@@ -32,6 +39,8 @@ export interface CodeFile {
32
39
  sizeBytes?: number;
33
40
  indexedAt: string;
34
41
  gitCommit?: string;
42
+ source?: CodeEvidenceSource;
43
+ parserError?: string;
35
44
  snapshotId?: string;
36
45
  sourceEpoch?: number;
37
46
  }
@@ -50,6 +59,7 @@ export interface CodeSymbol {
50
59
  contentHash?: string;
51
60
  indexedAt: string;
52
61
  stale?: boolean;
62
+ source?: CodeEvidenceSource;
53
63
  snapshotId?: string;
54
64
  sourceEpoch?: number;
55
65
  }
@@ -64,6 +74,7 @@ export interface CodeEdge {
64
74
  type: CodeEdgeType;
65
75
  confidence: number;
66
76
  evidence?: string;
77
+ source?: CodeEvidenceSource;
67
78
  indexedAt: string;
68
79
  snapshotId?: string;
69
80
  sourceEpoch?: number;
@@ -162,6 +173,9 @@ export interface CodeGraphStatus {
162
173
  symbols: number;
163
174
  edges: number;
164
175
  refs: number;
176
+ semanticSymbols: number;
177
+ semanticEdges: number;
178
+ parserErrors: number;
165
179
  indexedAt?: string;
166
180
  latestSnapshot?: CodeStateSnapshot;
167
181
  }
@@ -195,7 +209,7 @@ export interface ExternalCodeGraphHealth {
195
209
 
196
210
  export interface CodeGraphProviderQuality {
197
211
  /** Provider that contributed task-scoped code evidence to this response. */
198
- selected: CodeGraphProviderKind;
212
+ selected: CodeGraphEvidenceProvider;
199
213
  /** Lite is heuristic; external outlines are only semantic after validation. */
200
214
  selectedQuality: 'heuristic' | 'semantic';
201
215
  mode: CodeGraphExternalMode;
@@ -212,6 +226,12 @@ export interface CodeGraphProviderQuality {
212
226
  supportedLanguages: string[];
213
227
  };
214
228
  external: ExternalCodeGraphHealth;
229
+ semantic: {
230
+ state: 'not-indexed' | 'ready' | 'partial';
231
+ symbols: number;
232
+ edges: number;
233
+ parserErrors: number;
234
+ };
215
235
  }
216
236
 
217
237
  export interface ExternalCodeGraphSymbol {
@@ -233,8 +253,8 @@ export interface ExternalCodeGraphRelation {
233
253
  }
234
254
 
235
255
  /** A bounded, non-source-code semantic outline returned for one task. */
236
- export interface ExternalCodeGraphOutline {
237
- provider: 'external';
256
+ export interface CodeGraphOutline {
257
+ provider: 'semantic' | 'external';
238
258
  entryPoints: ExternalCodeGraphSymbol[];
239
259
  relations: ExternalCodeGraphRelation[];
240
260
  relatedFiles: string[];
@@ -244,3 +264,5 @@ export interface ExternalCodeGraphOutline {
244
264
  files: number;
245
265
  };
246
266
  }
267
+
268
+ export type ExternalCodeGraphOutline = CodeGraphOutline & { provider: 'external' };
@@ -321,6 +321,12 @@ async function handleApi(
321
321
  break;
322
322
  }
323
323
 
324
+ case '/codegraph': {
325
+ const { getCodeGraphDashboardStatus } = await import('../codegraph/dashboard.js');
326
+ sendJson(res, await getCodeGraphDashboardStatus(effectiveDataDir, effectiveProjectId));
327
+ break;
328
+ }
329
+
324
330
  case '/observations': {
325
331
  const observations = filterDashboardObservations(
326
332
  await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
@@ -1133,12 +1133,100 @@ body {
1133
1133
  padding: 16px 20px 8px;
1134
1134
  }
1135
1135
 
1136
+ #codegraph-health-panel {
1137
+ flex-shrink: 0;
1138
+ padding: 0 20px 10px;
1139
+ }
1140
+
1141
+ .codegraph-health {
1142
+ display: flex;
1143
+ align-items: center;
1144
+ justify-content: space-between;
1145
+ gap: 16px;
1146
+ min-width: 0;
1147
+ padding: 9px 12px;
1148
+ border: 1px solid var(--border-subtle);
1149
+ border-left: 3px solid var(--accent-green);
1150
+ border-radius: var(--radius-sm);
1151
+ background: var(--bg-surface);
1152
+ color: var(--text-secondary);
1153
+ font-size: 11px;
1154
+ }
1155
+
1156
+ .codegraph-health--partial {
1157
+ border-left-color: var(--accent-amber);
1158
+ }
1159
+
1160
+ .codegraph-health--not-indexed,
1161
+ .codegraph-health--unknown {
1162
+ border-left-color: var(--text-muted);
1163
+ }
1164
+
1165
+ .codegraph-health-title,
1166
+ .codegraph-health-metrics {
1167
+ display: flex;
1168
+ align-items: center;
1169
+ gap: 9px;
1170
+ min-width: 0;
1171
+ }
1172
+
1173
+ .codegraph-health-title strong {
1174
+ color: var(--text-primary);
1175
+ white-space: nowrap;
1176
+ }
1177
+
1178
+ .codegraph-health-title > span:not(.codegraph-health-dot) {
1179
+ overflow: hidden;
1180
+ text-overflow: ellipsis;
1181
+ white-space: nowrap;
1182
+ }
1183
+
1184
+ .codegraph-health-dot {
1185
+ width: 7px;
1186
+ height: 7px;
1187
+ flex: 0 0 7px;
1188
+ border-radius: 50%;
1189
+ background: var(--accent-green);
1190
+ }
1191
+
1192
+ .codegraph-health--partial .codegraph-health-dot {
1193
+ background: var(--accent-amber);
1194
+ }
1195
+
1196
+ .codegraph-health--not-indexed .codegraph-health-dot,
1197
+ .codegraph-health--unknown .codegraph-health-dot {
1198
+ background: var(--text-muted);
1199
+ }
1200
+
1201
+ .codegraph-health-metrics {
1202
+ flex-wrap: wrap;
1203
+ justify-content: flex-end;
1204
+ color: var(--text-muted);
1205
+ font-family: var(--font-mono);
1206
+ }
1207
+
1208
+ .codegraph-health-error {
1209
+ color: var(--accent-amber);
1210
+ }
1211
+
1136
1212
  .kg-page-shell .graph-layout {
1137
1213
  flex: 1;
1138
1214
  min-height: 0;
1215
+ height: auto;
1139
1216
  }
1140
1217
 
1141
1218
  @media (max-width: 768px) {
1219
+ #codegraph-health-panel {
1220
+ padding: 0 12px 10px;
1221
+ }
1222
+ .codegraph-health {
1223
+ align-items: flex-start;
1224
+ flex-direction: column;
1225
+ gap: 6px;
1226
+ }
1227
+ .codegraph-health-metrics {
1228
+ justify-content: flex-start;
1229
+ }
1142
1230
  .kg-page-shell .graph-layout {
1143
1231
  flex-direction: column;
1144
1232
  height: auto;