@psnext/lscg 0.1.4 → 0.1.6

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 (62) hide show
  1. package/README.md +117 -15
  2. package/dist/bin/lscg.js +0 -0
  3. package/dist/src/cli-progress.d.ts +7 -0
  4. package/dist/src/cli-progress.js +59 -0
  5. package/dist/src/cli.js +62 -9
  6. package/dist/src/explore/sigma-provider.d.ts +27 -0
  7. package/dist/src/explore/sigma-provider.js +87 -0
  8. package/dist/src/explore/sigma-render.d.ts +18 -0
  9. package/dist/src/explore/sigma-render.js +67 -0
  10. package/dist/src/graph/attribution.d.ts +2 -2
  11. package/dist/src/graph/attribution.js +36 -13
  12. package/dist/src/graph/explore.d.ts +20 -0
  13. package/dist/src/graph/explore.js +200 -0
  14. package/dist/src/graph/repository.d.ts +36 -4
  15. package/dist/src/graph/repository.js +443 -159
  16. package/dist/src/graph/repositoryScanWorker.d.ts +21 -0
  17. package/dist/src/graph/repositoryScanWorker.js +45 -0
  18. package/dist/src/index.d.ts +6 -0
  19. package/dist/src/index.js +5 -0
  20. package/dist/src/mcp/server.js +21 -3
  21. package/dist/src/parser/treeSitter.js +20 -1
  22. package/dist/src/scanner/artifactInventory.d.ts +35 -0
  23. package/dist/src/scanner/artifactInventory.js +139 -0
  24. package/dist/src/scanner/attributionPlugin.d.ts +5 -0
  25. package/dist/src/scanner/attributionPlugin.js +16 -0
  26. package/dist/src/scanner/discover.js +89 -16
  27. package/dist/src/scanner/fingerprint.js +5 -0
  28. package/dist/src/scanner/javaDependencyPlugin.d.ts +5 -0
  29. package/dist/src/scanner/javaDependencyPlugin.js +107 -0
  30. package/dist/src/scanner/javaPlugin.d.ts +5 -0
  31. package/dist/src/scanner/javaPlugin.js +199 -0
  32. package/dist/src/scanner/javaScanWorker.d.ts +2 -0
  33. package/dist/src/scanner/javaScanWorker.js +8 -0
  34. package/dist/src/scanner/packageParseWorker.d.ts +17 -0
  35. package/dist/src/scanner/packageParseWorker.js +30 -0
  36. package/dist/src/scanner/packagePlugin.js +83 -24
  37. package/dist/src/scanner/parallelScan.d.ts +2 -0
  38. package/dist/src/scanner/parallelScan.js +32 -0
  39. package/dist/src/scanner/plugins.d.ts +38 -4
  40. package/dist/src/scanner/plugins.js +58 -5
  41. package/dist/src/scanner/pythonPlugin.d.ts +5 -0
  42. package/dist/src/scanner/pythonPlugin.js +198 -0
  43. package/dist/src/scanner/pythonScanWorker.d.ts +2 -0
  44. package/dist/src/scanner/pythonScanWorker.js +8 -0
  45. package/dist/src/storage/connection.js +85 -0
  46. package/dist/src/storage/database.d.ts +1 -0
  47. package/dist/src/storage/database.js +1 -0
  48. package/dist/src/storage/explore-queries.d.ts +52 -0
  49. package/dist/src/storage/explore-queries.js +184 -0
  50. package/dist/src/storage/graph-writes.d.ts +22 -3
  51. package/dist/src/storage/graph-writes.js +167 -20
  52. package/dist/src/storage/manifest-inventory.d.ts +23 -0
  53. package/dist/src/storage/manifest-inventory.js +82 -0
  54. package/dist/src/storage/plugin-graph.js +3 -3
  55. package/dist/src/storage/queries.d.ts +10 -3
  56. package/dist/src/storage/queries.js +99 -24
  57. package/dist/src/storage/schema.d.ts +2 -2
  58. package/dist/src/storage/schema.js +48 -1
  59. package/dist/src/types.d.ts +110 -6
  60. package/dist/src/watch.d.ts +20 -2
  61. package/dist/src/watch.js +211 -46
  62. package/package.json +9 -3
@@ -1,11 +1,83 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import { parsePoint } from './row-decoders.js';
3
+ export function selectFreshnessReport(db, repositoryId) {
4
+ const scanState = db.prepare('SELECT state FROM repository_scan_state WHERE repository_id = ?').get(repositoryId);
5
+ if (!scanState?.state)
6
+ return { state: 'stale', reason: 'not_scanned' };
7
+ const failures = db.prepare('SELECT count(*) AS count FROM file_scan_failures WHERE repository_id = ?').get(repositoryId);
8
+ if (scanState.state === 'degraded' || failures.count > 0)
9
+ return { state: 'degraded', reason: 'retained_failure' };
10
+ if (scanState.state === 'stale')
11
+ return { state: 'stale', reason: 'known_invalidation' };
12
+ return { state: 'fresh' };
13
+ }
14
+ export function selectFileEnrichmentState(db, repositoryId, fileId) {
15
+ const row = db.prepare(`
16
+ SELECT repository_id AS repositoryId, file_id AS fileId, plugin_name AS pluginName,
17
+ source_hash AS sourceHash, history_fingerprint AS historyFingerprint,
18
+ state, outcome, diagnostic, queued_at AS queuedAt, updated_at AS updatedAt,
19
+ completed_at AS completedAt, failed_at AS failedAt
20
+ FROM file_enrichment_state
21
+ WHERE repository_id = ? AND file_id = ?
22
+ `).get(repositoryId, fileId);
23
+ return row ?? null;
24
+ }
25
+ export function selectFileEnrichmentStates(db, repositoryId, states = ['pending', 'failed']) {
26
+ if (states.length === 0)
27
+ return [];
28
+ const placeholders = states.map(() => '?').join(', ');
29
+ return db.prepare(`
30
+ SELECT repository_id AS repositoryId, file_id AS fileId, plugin_name AS pluginName,
31
+ source_hash AS sourceHash, history_fingerprint AS historyFingerprint,
32
+ state, outcome, diagnostic, queued_at AS queuedAt, updated_at AS updatedAt,
33
+ completed_at AS completedAt, failed_at AS failedAt
34
+ FROM file_enrichment_state
35
+ WHERE repository_id = ? AND state IN (${placeholders})
36
+ ORDER BY queued_at, file_id
37
+ `).all(repositoryId, ...states);
38
+ }
39
+ export function selectEnrichmentWork(db, repositoryId, states = ['pending', 'failed']) {
40
+ if (states.length === 0)
41
+ return [];
42
+ const placeholders = states.map(() => '?').join(', ');
43
+ return db.prepare(`
44
+ SELECT e.repository_id AS repositoryId, e.file_id AS fileId, e.plugin_name AS pluginName,
45
+ e.source_hash AS sourceHash, e.history_fingerprint AS historyFingerprint,
46
+ e.state, e.outcome, e.diagnostic, e.queued_at AS queuedAt, e.updated_at AS updatedAt,
47
+ e.completed_at AS completedAt, e.failed_at AS failedAt, f.path, f.language
48
+ FROM file_enrichment_state e
49
+ JOIN files f ON f.id = e.file_id
50
+ WHERE e.repository_id = ? AND e.state IN (${placeholders})
51
+ ORDER BY e.queued_at, e.file_id
52
+ `).all(repositoryId, ...states);
53
+ }
54
+ export function selectEnrichmentSummary(db, repositoryId) {
55
+ const states = selectFileEnrichmentStates(db, repositoryId, ['pending', 'complete', 'failed']);
56
+ const failedEntries = states.filter((entry) => entry.state === 'failed');
57
+ const unavailable = states.filter((entry) => entry.outcome === 'unavailable');
58
+ return {
59
+ state: states.length === 0 ? 'disabled' : failedEntries.length > 0 ? 'failed' : states.some((entry) => entry.state === 'pending') ? 'pending' : 'complete',
60
+ pending: states.filter((entry) => entry.state === 'pending').length,
61
+ complete: states.filter((entry) => entry.state === 'complete' && entry.outcome !== 'unavailable').length,
62
+ failed: failedEntries.length,
63
+ unavailable: unavailable.length,
64
+ notApplicable: unavailable.filter((entry) => entry.diagnostic === 'unavailable:not_applicable').length,
65
+ diagnostics: failedEntries.flatMap((entry) => entry.diagnostic ? [entry.diagnostic] : [])
66
+ };
67
+ }
3
68
  export function getStatus(db, repositoryId) {
4
69
  const repository = db.prepare('SELECT * FROM repositories WHERE id = ?').get(repositoryId);
5
70
  const files = db.prepare('SELECT count(*) AS count FROM files WHERE repository_id = ?').get(repositoryId).count;
6
71
  const nodes = db.prepare('SELECT count(*) AS count FROM nodes WHERE repository_id = ?').get(repositoryId).count;
7
72
  const edges = db.prepare('SELECT count(*) AS count FROM edges WHERE repository_id = ?').get(repositoryId).count;
8
- return { repository: repository ?? null, files, nodes, edges };
73
+ return {
74
+ repository: repository ?? null,
75
+ files,
76
+ nodes,
77
+ edges,
78
+ freshness: selectFreshnessReport(db, repositoryId),
79
+ enrichment: selectEnrichmentSummary(db, repositoryId)
80
+ };
9
81
  }
10
82
  export function selectNodes(db, repositoryId, { kind, limit = 50 } = {}) {
11
83
  const safeLimit = clampLimit(limit);
@@ -87,40 +159,43 @@ export function selectAllNodes(db, repositoryId, { kind } = {}) {
87
159
  ORDER BY kind, name IS NULL, name, id
88
160
  `).all(repositoryId);
89
161
  }
90
- export function selectEdges(db, repositoryId, { kind, limit = 50 } = {}) {
162
+ export function selectEdges(db, repositoryId, { kind, type, limit = 50 } = {}) {
91
163
  const safeLimit = clampLimit(limit);
164
+ const where = ['repository_id = ?'];
165
+ const parameters = [repositoryId];
92
166
  if (kind) {
93
- return db.prepare(`
94
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
95
- FROM edges
96
- WHERE repository_id = ? AND kind = ?
97
- ORDER BY kind, id
98
- LIMIT ?
99
- `).all(repositoryId, kind, safeLimit);
167
+ where.push('kind = ?');
168
+ parameters.push(kind);
169
+ }
170
+ if (type) {
171
+ where.push('type = ?');
172
+ parameters.push(type);
100
173
  }
101
174
  return db.prepare(`
102
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
175
+ SELECT id, kind, type, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
103
176
  FROM edges
104
- WHERE repository_id = ?
105
- ORDER BY kind, id
177
+ WHERE ${where.join(' AND ')}
178
+ ORDER BY kind, type, id
106
179
  LIMIT ?
107
- `).all(repositoryId, safeLimit);
180
+ `).all(...parameters, safeLimit);
108
181
  }
109
- export function selectAllEdges(db, repositoryId, { kind } = {}) {
182
+ export function selectAllEdges(db, repositoryId, { kind, type } = {}) {
183
+ const where = ['repository_id = ?'];
184
+ const parameters = [repositoryId];
110
185
  if (kind) {
111
- return db.prepare(`
112
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
113
- FROM edges
114
- WHERE repository_id = ? AND kind = ?
115
- ORDER BY kind, id
116
- `).all(repositoryId, kind);
186
+ where.push('kind = ?');
187
+ parameters.push(kind);
188
+ }
189
+ if (type) {
190
+ where.push('type = ?');
191
+ parameters.push(type);
117
192
  }
118
193
  return db.prepare(`
119
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
194
+ SELECT id, kind, type, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
120
195
  FROM edges
121
- WHERE repository_id = ?
122
- ORDER BY kind, id
123
- `).all(repositoryId);
196
+ WHERE ${where.join(' AND ')}
197
+ ORDER BY kind, type, id
198
+ `).all(...parameters);
124
199
  }
125
200
  export function clampLimit(limit, max = 500) {
126
201
  const value = Number(limit) || 50;
@@ -1,4 +1,4 @@
1
- export declare const SCHEMA_VERSION = 4;
2
- export declare const CREATE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS schema_migrations (\n version INTEGER PRIMARY KEY,\n applied_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS repositories (\n id TEXT PRIMARY KEY,\n root TEXT NOT NULL,\n name TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS files (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n language TEXT NOT NULL,\n hash TEXT NOT NULL,\n size INTEGER NOT NULL,\n mtime_ms INTEGER NOT NULL,\n scanned_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(repository_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT,\n start_byte INTEGER NOT NULL,\n end_byte INTEGER NOT NULL,\n start_point TEXT NOT NULL,\n end_point TEXT NOT NULL,\n source_hash TEXT NOT NULL,\n parser TEXT NOT NULL,\n parser_version TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n last_modified_user_id TEXT\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n metadata_json TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS overlays (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n subject_id TEXT NOT NULL,\n subject_kind TEXT NOT NULL,\n operation TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_files_repo_path ON files(repository_id, path);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_kind ON nodes(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_name ON nodes(repository_id, name);\nCREATE INDEX IF NOT EXISTS idx_edges_repo_kind ON edges(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);\nCREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);\n\nCREATE TABLE IF NOT EXISTS plugin_contributions (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n plugin_name TEXT NOT NULL,\n generation INTEGER NOT NULL,\n nodes_json TEXT NOT NULL,\n edges_json TEXT NOT NULL,\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, plugin_name)\n);\nCREATE INDEX IF NOT EXISTS idx_plugin_contributions_repo ON plugin_contributions(repository_id);\n";
1
+ export declare const SCHEMA_VERSION = 9;
2
+ export declare const CREATE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS schema_migrations (\n version INTEGER PRIMARY KEY,\n applied_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS repositories (\n id TEXT PRIMARY KEY,\n root TEXT NOT NULL,\n name TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS files (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n language TEXT NOT NULL,\n hash TEXT NOT NULL,\n size INTEGER NOT NULL,\n mtime_ms INTEGER NOT NULL,\n scanned_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(repository_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT,\n start_byte INTEGER NOT NULL,\n end_byte INTEGER NOT NULL,\n start_point TEXT NOT NULL,\n end_point TEXT NOT NULL,\n source_hash TEXT NOT NULL,\n parser TEXT NOT NULL,\n parser_version TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n last_modified_user_id TEXT\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n metadata_json TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS overlays (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n subject_id TEXT NOT NULL,\n subject_kind TEXT NOT NULL,\n operation TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_files_repo_path ON files(repository_id, path);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_kind ON nodes(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_name ON nodes(repository_id, name);\nCREATE INDEX IF NOT EXISTS idx_edges_repo_kind ON edges(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);\nCREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);\n\nCREATE TABLE IF NOT EXISTS plugin_contributions (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n plugin_name TEXT NOT NULL,\n generation INTEGER NOT NULL,\n nodes_json TEXT NOT NULL,\n edges_json TEXT NOT NULL,\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, plugin_name)\n);\nCREATE INDEX IF NOT EXISTS idx_plugin_contributions_repo ON plugin_contributions(repository_id);\n\nCREATE TABLE IF NOT EXISTS file_scan_failures (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n diagnostic TEXT NOT NULL,\n failed_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS repository_scan_state (\n repository_id TEXT PRIMARY KEY REFERENCES repositories(id) ON DELETE CASCADE,\n state TEXT NOT NULL CHECK(state IN ('fresh', 'degraded', 'stale')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS file_enrichment_state (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,\n plugin_name TEXT NOT NULL DEFAULT 'git-attribution-plugin',\n source_hash TEXT NOT NULL,\n history_fingerprint TEXT,\n state TEXT NOT NULL CHECK(state IN ('pending', 'complete', 'failed')),\n outcome TEXT CHECK(outcome IN ('complete', 'unavailable', 'failed')),\n diagnostic TEXT,\n queued_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT,\n failed_at TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_file_enrichment_state_repo_state ON file_enrichment_state(repository_id, state);\n\nCREATE TABLE IF NOT EXISTS repository_manifest_inventory (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n kind TEXT NOT NULL CHECK(kind IN ('maven-pom', 'gradle-groovy', 'gradle-kotlin')),\n module_identity TEXT NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('observed', 'read_failed', 'traversal_incomplete', 'confirmed_deleted')),\n size INTEGER NOT NULL,\n mtime_ms INTEGER NOT NULL,\n source_hash TEXT,\n generation INTEGER NOT NULL,\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, path)\n);\nCREATE INDEX IF NOT EXISTS idx_repository_manifest_inventory_repo_status\n ON repository_manifest_inventory(repository_id, status);\n";
3
3
  export declare const CREATE_NODES_TABLE_SQL = "\nCREATE TABLE nodes_new (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT,\n start_byte INTEGER NOT NULL,\n end_byte INTEGER NOT NULL,\n start_point TEXT NOT NULL,\n end_point TEXT NOT NULL,\n source_hash TEXT NOT NULL,\n parser TEXT NOT NULL,\n parser_version TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n last_modified_user_id TEXT\n);\n";
4
4
  //# sourceMappingURL=schema.d.ts.map
@@ -1,4 +1,4 @@
1
- export const SCHEMA_VERSION = 4;
1
+ export const SCHEMA_VERSION = 9;
2
2
  export const CREATE_SCHEMA_SQL = `
3
3
  CREATE TABLE IF NOT EXISTS schema_migrations (
4
4
  version INTEGER PRIMARY KEY,
@@ -50,6 +50,7 @@ CREATE TABLE IF NOT EXISTS edges (
50
50
  source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
51
51
  target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
52
52
  kind TEXT NOT NULL,
53
+ type TEXT NOT NULL,
53
54
  confidence REAL NOT NULL DEFAULT 1.0,
54
55
  metadata_json TEXT NOT NULL DEFAULT '{}'
55
56
  );
@@ -81,6 +82,52 @@ CREATE TABLE IF NOT EXISTS plugin_contributions (
81
82
  PRIMARY KEY(repository_id, plugin_name)
82
83
  );
83
84
  CREATE INDEX IF NOT EXISTS idx_plugin_contributions_repo ON plugin_contributions(repository_id);
85
+
86
+ CREATE TABLE IF NOT EXISTS file_scan_failures (
87
+ repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
88
+ path TEXT NOT NULL,
89
+ diagnostic TEXT NOT NULL,
90
+ failed_at TEXT NOT NULL DEFAULT (datetime('now')),
91
+ PRIMARY KEY(repository_id, path)
92
+ );
93
+
94
+ CREATE TABLE IF NOT EXISTS repository_scan_state (
95
+ repository_id TEXT PRIMARY KEY REFERENCES repositories(id) ON DELETE CASCADE,
96
+ state TEXT NOT NULL CHECK(state IN ('fresh', 'degraded', 'stale')),
97
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
98
+ );
99
+
100
+ CREATE TABLE IF NOT EXISTS file_enrichment_state (
101
+ repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
102
+ file_id TEXT PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
103
+ plugin_name TEXT NOT NULL DEFAULT 'git-attribution-plugin',
104
+ source_hash TEXT NOT NULL,
105
+ history_fingerprint TEXT,
106
+ state TEXT NOT NULL CHECK(state IN ('pending', 'complete', 'failed')),
107
+ outcome TEXT CHECK(outcome IN ('complete', 'unavailable', 'failed')),
108
+ diagnostic TEXT,
109
+ queued_at TEXT NOT NULL DEFAULT (datetime('now')),
110
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
111
+ completed_at TEXT,
112
+ failed_at TEXT
113
+ );
114
+ CREATE INDEX IF NOT EXISTS idx_file_enrichment_state_repo_state ON file_enrichment_state(repository_id, state);
115
+
116
+ CREATE TABLE IF NOT EXISTS repository_manifest_inventory (
117
+ repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
118
+ path TEXT NOT NULL,
119
+ kind TEXT NOT NULL CHECK(kind IN ('maven-pom', 'gradle-groovy', 'gradle-kotlin')),
120
+ module_identity TEXT NOT NULL,
121
+ status TEXT NOT NULL CHECK(status IN ('observed', 'read_failed', 'traversal_incomplete', 'confirmed_deleted')),
122
+ size INTEGER NOT NULL,
123
+ mtime_ms INTEGER NOT NULL,
124
+ source_hash TEXT,
125
+ generation INTEGER NOT NULL,
126
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
127
+ PRIMARY KEY(repository_id, path)
128
+ );
129
+ CREATE INDEX IF NOT EXISTS idx_repository_manifest_inventory_repo_status
130
+ ON repository_manifest_inventory(repository_id, status);
84
131
  `;
85
132
  export const CREATE_NODES_TABLE_SQL = `
86
133
  CREATE TABLE nodes_new (
@@ -40,6 +40,7 @@ export interface GraphEdge {
40
40
  sourceId: string;
41
41
  targetId: string;
42
42
  kind: GraphEdgeKind;
43
+ type?: string;
43
44
  confidence: number;
44
45
  metadata: Record<string, unknown>;
45
46
  }
@@ -73,6 +74,57 @@ export interface FileAttribution {
73
74
  contributorEmails: ContributorIdentity[];
74
75
  nodeAttributions: NodeAttribution[];
75
76
  }
77
+ /** The result of attempting optional Git attribution for one file. */
78
+ export type AttributionOutcome = {
79
+ status: 'complete';
80
+ attribution: FileAttribution;
81
+ } | {
82
+ status: 'unavailable';
83
+ reason: 'not_applicable' | 'unavailable';
84
+ } | {
85
+ status: 'failed';
86
+ diagnostic: string;
87
+ };
88
+ export type EnrichmentState = 'disabled' | 'pending' | 'complete' | 'failed';
89
+ export type EnrichmentOutcomeKind = 'complete' | 'unavailable' | 'failed';
90
+ /** Durable, hash-bound optional-enrichment work for a scanned file. */
91
+ export interface FileEnrichmentState {
92
+ repositoryId: string;
93
+ fileId: string;
94
+ pluginName: string;
95
+ sourceHash: string;
96
+ historyFingerprint: string | null;
97
+ state: Exclude<EnrichmentState, 'disabled'>;
98
+ outcome: EnrichmentOutcomeKind | null;
99
+ diagnostic: string | null;
100
+ queuedAt: string;
101
+ updatedAt: string;
102
+ completedAt: string | null;
103
+ failedAt: string | null;
104
+ }
105
+ export interface EnrichmentWork extends FileEnrichmentState {
106
+ path: string;
107
+ language: string;
108
+ }
109
+ export type AttributionApplyResult = 'applied' | 'stale';
110
+ /** Input supplied to an optional attribution runner for hash-compatible work. */
111
+ export interface EnrichmentRunnerInput {
112
+ repository: RepositoryRecord;
113
+ work: EnrichmentWork;
114
+ source: string;
115
+ nodes: GraphNode[];
116
+ }
117
+ /** Injectable optional-attribution implementation used when enrichment is drained. */
118
+ export type EnrichmentRunner = (input: EnrichmentRunnerInput) => AttributionOutcome | Promise<AttributionOutcome>;
119
+ export interface EnrichmentSummary {
120
+ state: EnrichmentState;
121
+ pending: number;
122
+ complete: number;
123
+ failed: number;
124
+ unavailable?: number;
125
+ notApplicable?: number;
126
+ diagnostics: string[];
127
+ }
76
128
  export interface ScanSummary {
77
129
  repository: RepositoryRecord;
78
130
  scopes: Array<{
@@ -84,10 +136,56 @@ export interface ScanSummary {
84
136
  nodesWritten: number;
85
137
  edgesWritten: number;
86
138
  skipped: string[];
139
+ enrichment: EnrichmentSummary;
87
140
  pluginDiagnostics?: string[];
88
141
  failedPlugins?: string[];
89
142
  stalePlugins?: string[];
90
143
  }
144
+ /**
145
+ * Internal, transport-neutral facts emitted by an explicit scan. These are
146
+ * deliberately aggregate/lifecycle events rather than a public wire format.
147
+ * In particular, counters describe one scan operation (including scope=both),
148
+ * not one database handle.
149
+ */
150
+ export type ScanProgressOperation = 'incremental' | 'full';
151
+ export type ScanProgressPhase = 'discovery' | 'plugins' | 'files' | 'reconciliation' | 'enrichment' | 'terminal';
152
+ export type ScanProgressStatus = 'started' | 'completed' | 'processed' | 'skipped' | 'deferred' | 'failed' | 'retrying' | 'degraded';
153
+ export interface ScanProgressEvent {
154
+ kind: 'scan';
155
+ operation: ScanProgressOperation;
156
+ scope: GraphScope;
157
+ phase: ScanProgressPhase;
158
+ status: ScanProgressStatus;
159
+ repository: RepositoryRecord;
160
+ attempt?: number;
161
+ detail?: string;
162
+ path?: string;
163
+ counts?: {
164
+ discovered?: number;
165
+ selected?: number;
166
+ unchanged?: number;
167
+ deleted?: number;
168
+ processed?: number;
169
+ skipped?: number;
170
+ plugins?: number;
171
+ failedPlugins?: number;
172
+ stalePlugins?: number;
173
+ pending?: number;
174
+ complete?: number;
175
+ failed?: number;
176
+ };
177
+ }
178
+ export type WatchProgressEventName = 'watching' | 'initial-scan' | 'rescan' | 'coalesced-change' | 'follow-up' | 'waiting' | 'polling-fallback' | 'enrichment' | 'stopped' | 'failed';
179
+ export interface WatchProgressEvent {
180
+ kind: 'watch';
181
+ event: WatchProgressEventName;
182
+ mode: 'watch' | 'scan --watch';
183
+ repository: RepositoryRecord;
184
+ scope: StorageScope;
185
+ detail?: string;
186
+ }
187
+ export type ProgressEvent = ScanProgressEvent | WatchProgressEvent;
188
+ export type ProgressReporter = (event: ProgressEvent) => void;
91
189
  export interface RepositoryInfo {
92
190
  id: string;
93
191
  root: string;
@@ -112,6 +210,7 @@ export interface GraphNodeTextRow extends GraphNodeRow {
112
210
  export interface GraphEdgeRow {
113
211
  id: string;
114
212
  kind: GraphEdgeKind;
213
+ type?: string;
115
214
  sourceId: string;
116
215
  targetId: string;
117
216
  fileId: string | null;
@@ -142,15 +241,24 @@ export interface CallGraphMatch extends GraphNodeTextRow {
142
241
  upstream: CallGraphRelation[];
143
242
  downstream: CallGraphRelation[];
144
243
  }
244
+ export type FreshnessState = 'fresh' | 'stale' | 'degraded';
245
+ export type FreshnessReason = 'not_scanned' | 'known_invalidation' | 'retained_failure';
246
+ /** Persisted graph freshness, evaluated only from stored scan state. */
247
+ export interface FreshnessReport {
248
+ state: FreshnessState;
249
+ reason?: FreshnessReason | undefined;
250
+ }
145
251
  export interface StatusResult {
146
252
  repository: RepositoryRecord | null;
147
253
  files: number;
148
254
  nodes: number;
149
255
  edges: number;
256
+ freshness: FreshnessReport;
257
+ enrichment: EnrichmentSummary;
150
258
  }
151
259
  /** Stable, transport-neutral contract returned by the context retrieval command. */
152
260
  export type ContextResultState = 'ok' | 'ambiguous' | 'not_found' | 'refresh_failed' | 'stale';
153
- export type ContextFreshnessState = 'unchanged' | 'refreshed' | 'stale' | 'refresh_failed';
261
+ export type ContextFreshnessState = FreshnessState;
154
262
  export type ContextFreshnessVerification = 'metadata_only' | 'content_hash';
155
263
  export type ContextRelationCategory = 'definition' | 'impact' | 'dependency';
156
264
  export interface ContextBudget {
@@ -224,11 +332,7 @@ export interface ContextScopeResult {
224
332
  warnings: string[];
225
333
  truncation: ContextTruncation[];
226
334
  };
227
- freshness: {
228
- state: ContextFreshnessState;
229
- verification: ContextFreshnessVerification;
230
- refresh: ContextRefreshSummary;
231
- };
335
+ freshness: FreshnessReport;
232
336
  }
233
337
  export interface ContextGraphResult {
234
338
  contract_version: 1;
@@ -1,9 +1,13 @@
1
- import { scanRepository } from './graph/repository.js';
2
- import type { RepositoryRecord, StorageScope } from './types.js';
1
+ import { drainRepositoryEnrichment, scanRepository } from './graph/repository.js';
2
+ import type { EnrichmentSummary, ProgressReporter, RepositoryRecord, StorageScope } from './types.js';
3
+ import type { ScannerPlugin } from './scanner/plugins.js';
3
4
  export interface WatchCommandOptions {
4
5
  root?: string | undefined;
5
6
  scope?: StorageScope | 'both' | undefined;
7
+ attribution?: boolean | undefined;
8
+ plugins?: readonly ScannerPlugin[] | undefined;
6
9
  intervalMs?: number | undefined;
10
+ debounceMs?: number | undefined;
7
11
  }
8
12
  export interface WatchSummary {
9
13
  filesDiscovered: number;
@@ -11,6 +15,7 @@ export interface WatchSummary {
11
15
  nodesWritten: number;
12
16
  edgesWritten: number;
13
17
  skippedCount: number;
18
+ enrichment: EnrichmentSummary;
14
19
  }
15
20
  export interface WatchEvent {
16
21
  event: 'watching' | 'scan' | 'waiting' | 'rescan-triggered' | 'stopped';
@@ -22,13 +27,26 @@ export interface WatchEvent {
22
27
  summary?: WatchSummary | undefined;
23
28
  scans?: number | undefined;
24
29
  }
30
+ export type FileSystemWatchEvent = 'change' | 'rename';
31
+ export type FileSystemWatchListener = (event: FileSystemWatchEvent, filename: string | Buffer | null) => void;
32
+ /** Minimal fs.watch surface so watch behavior can be tested without the filesystem. */
33
+ export interface FileSystemWatcher {
34
+ close(): void;
35
+ on(event: 'error', listener: (error: Error) => void): this;
36
+ }
37
+ export type FileSystemWatcherFactory = (directory: string, options: {
38
+ recursive: boolean;
39
+ }, listener: FileSystemWatchListener) => FileSystemWatcher;
25
40
  export interface WatchDependencies {
26
41
  scanRepository?: typeof scanRepository | undefined;
42
+ drainEnrichment?: typeof drainRepositoryEnrichment | undefined;
27
43
  fingerprint?: ((root: string) => Promise<string> | string) | undefined;
44
+ watcherFactory?: FileSystemWatcherFactory | undefined;
28
45
  emit?: ((event: WatchEvent) => void) | undefined;
29
46
  wait?: ((ms: number, signal?: AbortSignal) => Promise<void>) | undefined;
30
47
  signal?: AbortSignal | undefined;
31
48
  mode?: 'watch' | 'scan --watch' | undefined;
49
+ progress?: ProgressReporter | undefined;
32
50
  }
33
51
  export interface WatchRunResult {
34
52
  repository: RepositoryRecord;