@psnext/lscg 0.1.3 → 0.1.5

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 (52) hide show
  1. package/README.md +87 -9
  2. package/dist/src/cli-progress.d.ts +7 -0
  3. package/dist/src/cli-progress.js +59 -0
  4. package/dist/src/cli.js +14 -6
  5. package/dist/src/graph/attribution.d.ts +2 -2
  6. package/dist/src/graph/attribution.js +36 -13
  7. package/dist/src/graph/repository.d.ts +30 -3
  8. package/dist/src/graph/repository.js +396 -158
  9. package/dist/src/graph/repositoryScanWorker.d.ts +21 -0
  10. package/dist/src/graph/repositoryScanWorker.js +45 -0
  11. package/dist/src/index.d.ts +5 -0
  12. package/dist/src/index.js +4 -0
  13. package/dist/src/mcp/server.js +16 -1
  14. package/dist/src/parser/treeSitter.js +20 -1
  15. package/dist/src/scanner/artifactInventory.d.ts +35 -0
  16. package/dist/src/scanner/artifactInventory.js +139 -0
  17. package/dist/src/scanner/fingerprint.js +5 -0
  18. package/dist/src/scanner/javaDependencyPlugin.d.ts +5 -0
  19. package/dist/src/scanner/javaDependencyPlugin.js +107 -0
  20. package/dist/src/scanner/javaPlugin.d.ts +5 -0
  21. package/dist/src/scanner/javaPlugin.js +199 -0
  22. package/dist/src/scanner/javaScanWorker.d.ts +2 -0
  23. package/dist/src/scanner/javaScanWorker.js +8 -0
  24. package/dist/src/scanner/packageParseWorker.d.ts +17 -0
  25. package/dist/src/scanner/packageParseWorker.js +30 -0
  26. package/dist/src/scanner/packagePlugin.js +126 -24
  27. package/dist/src/scanner/parallelScan.d.ts +2 -0
  28. package/dist/src/scanner/parallelScan.js +32 -0
  29. package/dist/src/scanner/plugins.d.ts +32 -2
  30. package/dist/src/scanner/plugins.js +51 -5
  31. package/dist/src/scanner/pythonPlugin.d.ts +5 -0
  32. package/dist/src/scanner/pythonPlugin.js +198 -0
  33. package/dist/src/scanner/pythonScanWorker.d.ts +2 -0
  34. package/dist/src/scanner/pythonScanWorker.js +8 -0
  35. package/dist/src/storage/connection.js +63 -0
  36. package/dist/src/storage/database.d.ts +1 -0
  37. package/dist/src/storage/database.js +1 -0
  38. package/dist/src/storage/graph-writes.d.ts +16 -3
  39. package/dist/src/storage/graph-writes.js +142 -17
  40. package/dist/src/storage/manifest-inventory.d.ts +23 -0
  41. package/dist/src/storage/manifest-inventory.js +82 -0
  42. package/dist/src/storage/queries.d.ts +6 -1
  43. package/dist/src/storage/queries.js +67 -1
  44. package/dist/src/storage/schema.d.ts +2 -2
  45. package/dist/src/storage/schema.js +44 -1
  46. package/dist/src/types.d.ts +102 -6
  47. package/dist/src/view/templates/icons/call.svg +11 -9
  48. package/dist/src/view/templates/interactive.css +11 -8
  49. package/dist/src/view/templates/interactive.html +160 -46
  50. package/dist/src/watch.d.ts +17 -2
  51. package/dist/src/watch.js +208 -46
  52. package/package.json +9 -3
@@ -0,0 +1,23 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ManifestKind, ManifestStatus, RepositoryManifest } from '../scanner/artifactInventory.js';
3
+ export interface RepositoryManifestInventoryRow {
4
+ repositoryId: string;
5
+ path: string;
6
+ kind: ManifestKind;
7
+ moduleIdentity: string;
8
+ status: ManifestStatus;
9
+ size: number;
10
+ mtimeMs: number;
11
+ sourceHash: string | null;
12
+ generation: number;
13
+ }
14
+ export declare function selectManifestInventory(db: DatabaseSync, repositoryId: string): RepositoryManifestInventoryRow[];
15
+ /** Persist one complete inventory snapshot. Uncertain records remain queryable for restart-safe retention. */
16
+ export declare function persistManifestInventory(db: DatabaseSync, repositoryId: string, manifests: readonly RepositoryManifest[], generation: number): void;
17
+ /** Persist inventory and freshness together at the end of a structural scan. */
18
+ export declare function persistManifestInventoryAndScanState(db: DatabaseSync, repositoryId: string, manifests: readonly RepositoryManifest[], generation: number, state: 'fresh' | 'degraded' | 'stale'): void;
19
+ /** Descriptive aliases for callers that prefer the table's full name. */
20
+ export declare const selectRepositoryManifestInventory: typeof selectManifestInventory;
21
+ export declare const persistRepositoryManifestInventory: typeof persistManifestInventory;
22
+ export declare function manifestRowsAsRecords(rows: readonly RepositoryManifestInventoryRow[]): RepositoryManifest[];
23
+ //# sourceMappingURL=manifest-inventory.d.ts.map
@@ -0,0 +1,82 @@
1
+ export function selectManifestInventory(db, repositoryId) {
2
+ const rows = db.prepare(`
3
+ SELECT repository_id AS repositoryId, path, kind, module_identity AS moduleIdentity,
4
+ status, size, mtime_ms AS mtimeMs, source_hash AS sourceHash, generation
5
+ FROM repository_manifest_inventory
6
+ WHERE repository_id = ?
7
+ ORDER BY path
8
+ `).all(repositoryId);
9
+ return rows.map((row) => ({
10
+ repositoryId: String(row.repositoryId),
11
+ path: String(row.path),
12
+ kind: row.kind,
13
+ moduleIdentity: String(row.moduleIdentity),
14
+ status: row.status,
15
+ size: Number(row.size),
16
+ mtimeMs: Number(row.mtimeMs),
17
+ sourceHash: typeof row.sourceHash === 'string' ? row.sourceHash : null,
18
+ generation: Number(row.generation)
19
+ }));
20
+ }
21
+ /** Persist one complete inventory snapshot. Uncertain records remain queryable for restart-safe retention. */
22
+ export function persistManifestInventory(db, repositoryId, manifests, generation) {
23
+ inTransaction(db, () => writeManifestInventory(db, repositoryId, manifests, generation));
24
+ }
25
+ /** Persist inventory and freshness together at the end of a structural scan. */
26
+ export function persistManifestInventoryAndScanState(db, repositoryId, manifests, generation, state) {
27
+ inTransaction(db, () => {
28
+ writeManifestInventory(db, repositoryId, manifests, generation);
29
+ db.prepare(`
30
+ INSERT INTO repository_scan_state(repository_id, state, updated_at)
31
+ VALUES (?, ?, datetime('now'))
32
+ ON CONFLICT(repository_id) DO UPDATE SET state = excluded.state, updated_at = excluded.updated_at
33
+ `).run(repositoryId, state);
34
+ });
35
+ }
36
+ function writeManifestInventory(db, repositoryId, manifests, generation) {
37
+ const statement = db.prepare(`
38
+ INSERT INTO repository_manifest_inventory(
39
+ repository_id, path, kind, module_identity, status, size, mtime_ms, source_hash, generation
40
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
41
+ ON CONFLICT(repository_id, path) DO UPDATE SET
42
+ kind = excluded.kind,
43
+ module_identity = excluded.module_identity,
44
+ status = excluded.status,
45
+ size = excluded.size,
46
+ mtime_ms = excluded.mtime_ms,
47
+ source_hash = excluded.source_hash,
48
+ generation = excluded.generation,
49
+ updated_at = datetime('now')
50
+ `);
51
+ for (const manifest of manifests) {
52
+ statement.run(repositoryId, manifest.path, manifest.kind, manifest.moduleIdentity, manifest.status, manifest.size, manifest.mtimeMs, manifest.sourceHash, generation);
53
+ }
54
+ }
55
+ function inTransaction(db, work) {
56
+ db.exec('BEGIN IMMEDIATE');
57
+ try {
58
+ work();
59
+ db.exec('COMMIT');
60
+ }
61
+ catch (error) {
62
+ db.exec('ROLLBACK');
63
+ throw error;
64
+ }
65
+ }
66
+ /** Descriptive aliases for callers that prefer the table's full name. */
67
+ export const selectRepositoryManifestInventory = selectManifestInventory;
68
+ export const persistRepositoryManifestInventory = persistManifestInventory;
69
+ export function manifestRowsAsRecords(rows) {
70
+ return rows.map((row) => ({
71
+ path: row.path,
72
+ kind: row.kind,
73
+ moduleIdentity: row.moduleIdentity,
74
+ size: row.size,
75
+ mtimeMs: row.mtimeMs,
76
+ sourceHash: row.sourceHash,
77
+ content: null,
78
+ status: row.status,
79
+ generation: row.generation
80
+ }));
81
+ }
82
+ //# sourceMappingURL=manifest-inventory.js.map
@@ -1,5 +1,10 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
- import type { GraphEdgeRow, GraphNodeRow, GraphNodeTextRow, StatusResult } from '../types.js';
2
+ import type { EnrichmentState, EnrichmentSummary, EnrichmentWork, FileEnrichmentState, FreshnessReport, GraphEdgeRow, GraphNodeRow, GraphNodeTextRow, StatusResult } from '../types.js';
3
+ export declare function selectFreshnessReport(db: DatabaseSync, repositoryId: string): FreshnessReport;
4
+ export declare function selectFileEnrichmentState(db: DatabaseSync, repositoryId: string, fileId: string): FileEnrichmentState | null;
5
+ export declare function selectFileEnrichmentStates(db: DatabaseSync, repositoryId: string, states?: readonly EnrichmentState[]): FileEnrichmentState[];
6
+ export declare function selectEnrichmentWork(db: DatabaseSync, repositoryId: string, states?: readonly EnrichmentState[]): EnrichmentWork[];
7
+ export declare function selectEnrichmentSummary(db: DatabaseSync, repositoryId: string): EnrichmentSummary;
3
8
  export declare function getStatus(db: DatabaseSync, repositoryId: string): StatusResult;
4
9
  export declare function selectNodes(db: DatabaseSync, repositoryId: string, { kind, limit }?: {
5
10
  kind?: string | undefined;
@@ -1,11 +1,77 @@
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, source_hash AS sourceHash,
17
+ state, diagnostic, queued_at AS queuedAt, updated_at AS updatedAt,
18
+ completed_at AS completedAt, failed_at AS failedAt
19
+ FROM file_enrichment_state
20
+ WHERE repository_id = ? AND file_id = ?
21
+ `).get(repositoryId, fileId);
22
+ return row ?? null;
23
+ }
24
+ export function selectFileEnrichmentStates(db, repositoryId, states = ['pending', 'failed']) {
25
+ if (states.length === 0)
26
+ return [];
27
+ const placeholders = states.map(() => '?').join(', ');
28
+ return db.prepare(`
29
+ SELECT repository_id AS repositoryId, file_id AS fileId, source_hash AS sourceHash,
30
+ state, diagnostic, queued_at AS queuedAt, updated_at AS updatedAt,
31
+ completed_at AS completedAt, failed_at AS failedAt
32
+ FROM file_enrichment_state
33
+ WHERE repository_id = ? AND state IN (${placeholders})
34
+ ORDER BY queued_at, file_id
35
+ `).all(repositoryId, ...states);
36
+ }
37
+ export function selectEnrichmentWork(db, repositoryId, states = ['pending', 'failed']) {
38
+ if (states.length === 0)
39
+ return [];
40
+ const placeholders = states.map(() => '?').join(', ');
41
+ return db.prepare(`
42
+ SELECT e.repository_id AS repositoryId, e.file_id AS fileId, e.source_hash AS sourceHash,
43
+ e.state, e.diagnostic, e.queued_at AS queuedAt, e.updated_at AS updatedAt,
44
+ e.completed_at AS completedAt, e.failed_at AS failedAt, f.path, f.language
45
+ FROM file_enrichment_state e
46
+ JOIN files f ON f.id = e.file_id
47
+ WHERE e.repository_id = ? AND e.state IN (${placeholders})
48
+ ORDER BY e.queued_at, e.file_id
49
+ `).all(repositoryId, ...states);
50
+ }
51
+ export function selectEnrichmentSummary(db, repositoryId) {
52
+ const states = selectFileEnrichmentStates(db, repositoryId, ['pending', 'complete', 'failed']);
53
+ const failedEntries = states.filter((entry) => entry.state === 'failed');
54
+ return {
55
+ state: failedEntries.length > 0 ? 'failed' : states.some((entry) => entry.state === 'pending') ? 'pending' : 'complete',
56
+ pending: states.filter((entry) => entry.state === 'pending').length,
57
+ complete: states.filter((entry) => entry.state === 'complete').length,
58
+ failed: failedEntries.length,
59
+ diagnostics: failedEntries.flatMap((entry) => entry.diagnostic ? [entry.diagnostic] : [])
60
+ };
61
+ }
3
62
  export function getStatus(db, repositoryId) {
4
63
  const repository = db.prepare('SELECT * FROM repositories WHERE id = ?').get(repositoryId);
5
64
  const files = db.prepare('SELECT count(*) AS count FROM files WHERE repository_id = ?').get(repositoryId).count;
6
65
  const nodes = db.prepare('SELECT count(*) AS count FROM nodes WHERE repository_id = ?').get(repositoryId).count;
7
66
  const edges = db.prepare('SELECT count(*) AS count FROM edges WHERE repository_id = ?').get(repositoryId).count;
8
- return { repository: repository ?? null, files, nodes, edges };
67
+ return {
68
+ repository: repository ?? null,
69
+ files,
70
+ nodes,
71
+ edges,
72
+ freshness: selectFreshnessReport(db, repositoryId),
73
+ enrichment: selectEnrichmentSummary(db, repositoryId)
74
+ };
9
75
  }
10
76
  export function selectNodes(db, repositoryId, { kind, limit = 50 } = {}) {
11
77
  const safeLimit = clampLimit(limit);
@@ -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 = 7;
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\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 source_hash TEXT NOT NULL,\n state TEXT NOT NULL CHECK(state IN ('pending', 'complete', '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 = 7;
2
2
  export const CREATE_SCHEMA_SQL = `
3
3
  CREATE TABLE IF NOT EXISTS schema_migrations (
4
4
  version INTEGER PRIMARY KEY,
@@ -81,6 +81,49 @@ CREATE TABLE IF NOT EXISTS plugin_contributions (
81
81
  PRIMARY KEY(repository_id, plugin_name)
82
82
  );
83
83
  CREATE INDEX IF NOT EXISTS idx_plugin_contributions_repo ON plugin_contributions(repository_id);
84
+
85
+ CREATE TABLE IF NOT EXISTS file_scan_failures (
86
+ repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
87
+ path TEXT NOT NULL,
88
+ diagnostic TEXT NOT NULL,
89
+ failed_at TEXT NOT NULL DEFAULT (datetime('now')),
90
+ PRIMARY KEY(repository_id, path)
91
+ );
92
+
93
+ CREATE TABLE IF NOT EXISTS repository_scan_state (
94
+ repository_id TEXT PRIMARY KEY REFERENCES repositories(id) ON DELETE CASCADE,
95
+ state TEXT NOT NULL CHECK(state IN ('fresh', 'degraded', 'stale')),
96
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
97
+ );
98
+
99
+ CREATE TABLE IF NOT EXISTS file_enrichment_state (
100
+ repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
101
+ file_id TEXT PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
102
+ source_hash TEXT NOT NULL,
103
+ state TEXT NOT NULL CHECK(state IN ('pending', 'complete', 'failed')),
104
+ diagnostic TEXT,
105
+ queued_at TEXT NOT NULL DEFAULT (datetime('now')),
106
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
107
+ completed_at TEXT,
108
+ failed_at TEXT
109
+ );
110
+ CREATE INDEX IF NOT EXISTS idx_file_enrichment_state_repo_state ON file_enrichment_state(repository_id, state);
111
+
112
+ CREATE TABLE IF NOT EXISTS repository_manifest_inventory (
113
+ repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
114
+ path TEXT NOT NULL,
115
+ kind TEXT NOT NULL CHECK(kind IN ('maven-pom', 'gradle-groovy', 'gradle-kotlin')),
116
+ module_identity TEXT NOT NULL,
117
+ status TEXT NOT NULL CHECK(status IN ('observed', 'read_failed', 'traversal_incomplete', 'confirmed_deleted')),
118
+ size INTEGER NOT NULL,
119
+ mtime_ms INTEGER NOT NULL,
120
+ source_hash TEXT,
121
+ generation INTEGER NOT NULL,
122
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
123
+ PRIMARY KEY(repository_id, path)
124
+ );
125
+ CREATE INDEX IF NOT EXISTS idx_repository_manifest_inventory_repo_status
126
+ ON repository_manifest_inventory(repository_id, status);
84
127
  `;
85
128
  export const CREATE_NODES_TABLE_SQL = `
86
129
  CREATE TABLE nodes_new (
@@ -73,6 +73,51 @@ export interface FileAttribution {
73
73
  contributorEmails: ContributorIdentity[];
74
74
  nodeAttributions: NodeAttribution[];
75
75
  }
76
+ /** The result of attempting optional Git attribution for one file. */
77
+ export type AttributionOutcome = {
78
+ status: 'complete';
79
+ attribution: FileAttribution;
80
+ } | {
81
+ status: 'unavailable';
82
+ reason: 'not_applicable' | 'unavailable';
83
+ } | {
84
+ status: 'failed';
85
+ diagnostic: string;
86
+ };
87
+ export type EnrichmentState = 'pending' | 'complete' | 'failed';
88
+ /** Durable, hash-bound optional-enrichment work for a scanned file. */
89
+ export interface FileEnrichmentState {
90
+ repositoryId: string;
91
+ fileId: string;
92
+ sourceHash: string;
93
+ state: EnrichmentState;
94
+ diagnostic: string | null;
95
+ queuedAt: string;
96
+ updatedAt: string;
97
+ completedAt: string | null;
98
+ failedAt: string | null;
99
+ }
100
+ export interface EnrichmentWork extends FileEnrichmentState {
101
+ path: string;
102
+ language: string;
103
+ }
104
+ export type AttributionApplyResult = 'applied' | 'stale';
105
+ /** Input supplied to an optional attribution runner for hash-compatible work. */
106
+ export interface EnrichmentRunnerInput {
107
+ repository: RepositoryRecord;
108
+ work: EnrichmentWork;
109
+ source: string;
110
+ nodes: GraphNode[];
111
+ }
112
+ /** Injectable optional-attribution implementation used when enrichment is drained. */
113
+ export type EnrichmentRunner = (input: EnrichmentRunnerInput) => AttributionOutcome | Promise<AttributionOutcome>;
114
+ export interface EnrichmentSummary {
115
+ state: EnrichmentState;
116
+ pending: number;
117
+ complete: number;
118
+ failed: number;
119
+ diagnostics: string[];
120
+ }
76
121
  export interface ScanSummary {
77
122
  repository: RepositoryRecord;
78
123
  scopes: Array<{
@@ -84,10 +129,56 @@ export interface ScanSummary {
84
129
  nodesWritten: number;
85
130
  edgesWritten: number;
86
131
  skipped: string[];
132
+ enrichment: EnrichmentSummary;
87
133
  pluginDiagnostics?: string[];
88
134
  failedPlugins?: string[];
89
135
  stalePlugins?: string[];
90
136
  }
137
+ /**
138
+ * Internal, transport-neutral facts emitted by an explicit scan. These are
139
+ * deliberately aggregate/lifecycle events rather than a public wire format.
140
+ * In particular, counters describe one scan operation (including scope=both),
141
+ * not one database handle.
142
+ */
143
+ export type ScanProgressOperation = 'incremental' | 'full';
144
+ export type ScanProgressPhase = 'discovery' | 'plugins' | 'files' | 'reconciliation' | 'enrichment' | 'terminal';
145
+ export type ScanProgressStatus = 'started' | 'completed' | 'processed' | 'skipped' | 'deferred' | 'failed' | 'retrying' | 'degraded';
146
+ export interface ScanProgressEvent {
147
+ kind: 'scan';
148
+ operation: ScanProgressOperation;
149
+ scope: GraphScope;
150
+ phase: ScanProgressPhase;
151
+ status: ScanProgressStatus;
152
+ repository: RepositoryRecord;
153
+ attempt?: number;
154
+ detail?: string;
155
+ path?: string;
156
+ counts?: {
157
+ discovered?: number;
158
+ selected?: number;
159
+ unchanged?: number;
160
+ deleted?: number;
161
+ processed?: number;
162
+ skipped?: number;
163
+ plugins?: number;
164
+ failedPlugins?: number;
165
+ stalePlugins?: number;
166
+ pending?: number;
167
+ complete?: number;
168
+ failed?: number;
169
+ };
170
+ }
171
+ export type WatchProgressEventName = 'watching' | 'initial-scan' | 'rescan' | 'coalesced-change' | 'follow-up' | 'waiting' | 'polling-fallback' | 'enrichment' | 'stopped' | 'failed';
172
+ export interface WatchProgressEvent {
173
+ kind: 'watch';
174
+ event: WatchProgressEventName;
175
+ mode: 'watch' | 'scan --watch';
176
+ repository: RepositoryRecord;
177
+ scope: StorageScope;
178
+ detail?: string;
179
+ }
180
+ export type ProgressEvent = ScanProgressEvent | WatchProgressEvent;
181
+ export type ProgressReporter = (event: ProgressEvent) => void;
91
182
  export interface RepositoryInfo {
92
183
  id: string;
93
184
  root: string;
@@ -142,15 +233,24 @@ export interface CallGraphMatch extends GraphNodeTextRow {
142
233
  upstream: CallGraphRelation[];
143
234
  downstream: CallGraphRelation[];
144
235
  }
236
+ export type FreshnessState = 'fresh' | 'stale' | 'degraded';
237
+ export type FreshnessReason = 'not_scanned' | 'known_invalidation' | 'retained_failure';
238
+ /** Persisted graph freshness, evaluated only from stored scan state. */
239
+ export interface FreshnessReport {
240
+ state: FreshnessState;
241
+ reason?: FreshnessReason | undefined;
242
+ }
145
243
  export interface StatusResult {
146
244
  repository: RepositoryRecord | null;
147
245
  files: number;
148
246
  nodes: number;
149
247
  edges: number;
248
+ freshness: FreshnessReport;
249
+ enrichment: EnrichmentSummary;
150
250
  }
151
251
  /** Stable, transport-neutral contract returned by the context retrieval command. */
152
252
  export type ContextResultState = 'ok' | 'ambiguous' | 'not_found' | 'refresh_failed' | 'stale';
153
- export type ContextFreshnessState = 'unchanged' | 'refreshed' | 'stale' | 'refresh_failed';
253
+ export type ContextFreshnessState = FreshnessState;
154
254
  export type ContextFreshnessVerification = 'metadata_only' | 'content_hash';
155
255
  export type ContextRelationCategory = 'definition' | 'impact' | 'dependency';
156
256
  export interface ContextBudget {
@@ -224,11 +324,7 @@ export interface ContextScopeResult {
224
324
  warnings: string[];
225
325
  truncation: ContextTruncation[];
226
326
  };
227
- freshness: {
228
- state: ContextFreshnessState;
229
- verification: ContextFreshnessVerification;
230
- refresh: ContextRefreshSummary;
231
- };
327
+ freshness: FreshnessReport;
232
328
  }
233
329
  export interface ContextGraphResult {
234
330
  contract_version: 1;
@@ -1,11 +1,13 @@
1
- <svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g id="call_icon">
3
- <circle cx="24" cy="24" r="21" fill="#ff6d2e" stroke="inherit" stroke-width="4"></circle>
4
- <path d="M37 22.0001L34 25.0001L23 14.0001L26 11.0001C27.5 9.50002 33 7.00005 37 11.0001C41 15.0001 38.5 20.5 37 22.0001Z" fill="#b71111" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
5
- <path d="M42 6L37 11" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
6
- <path d="M11 25.9999L14 22.9999L25 33.9999L22 36.9999C20.5 38.5 15 41 11 36.9999C7 32.9999 9.5 27.5 11 25.9999Z" fill="#b71111" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
7
- <path d="M23 32L27 28" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
8
- <path d="M6 42L11 37" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
9
- <path d="M16 25L20 21" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
1
+ <svg viewBox="0 0 48 48" fill="none" stroke="#3a3a3a" xmlns="http://www.w3.org/2000/svg">
2
+ <g id="call_icon" >
3
+ <circle cx="24" cy="24" r="21" fill="#fb7185" stroke="inherit" stroke-width="2"></circle>
4
+ <path d="M37 22.0001L34 25.0001L23 14.0001L26 11.0001C27.5 9.50002 33 7.00005 37 11.0001C41 15.0001 38.5 20.5 37 22.0001Z"
5
+ fill="#fb7185" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
6
+ <path d="M42 6L37 11" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
7
+ <path d="M11 25.9999L14 22.9999L25 33.9999L22 36.9999C20.5 38.5 15 41 11 36.9999C7 32.9999 9.5 27.5 11 25.9999Z"
8
+ fill="#fb7185" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
9
+ <path d="M23 32L27 28" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
10
+ <path d="M6 42L11 37" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
11
+ <path d="M16 25L20 21" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
10
12
  </g>
11
13
  </svg>
@@ -193,20 +193,23 @@ html, body {
193
193
  font-weight: 200;
194
194
  paint-order: stroke;
195
195
  stroke: #020617;
196
- stroke-width: 3px;
196
+ stroke-width: 2px;
197
197
  stroke-linejoin: round;
198
198
  -webkit-user-select: none;
199
199
  user-select: none;
200
200
  pointer-events: none;
201
201
  }
202
- .node.is-highlighted .node-shape,
203
202
  .node.is-selected .node-shape {
204
- stroke: white;
205
- stroke-width: 2.5;
206
- }
207
- .node.is-hidden,
208
- .edge.is-hidden {
209
- display: none;
203
+ stroke: rgb(255, 255, 255);
204
+ stroke-width: 1;
205
+ filter:
206
+ drop-shadow(0px 0px 12px #00a2ff)
207
+ drop-shadow(0px 0px 24px #00a2ff)
208
+ drop-shadow(0px 0px 36px #00a2ff);
209
+ }
210
+ .node.is-highlighted .node.is-selected .node-shape {
211
+ stroke: rgb(193, 193, 193);
212
+ stroke-width: 2;
210
213
  }
211
214
  .node.is-dimmed {
212
215
  opacity: 0.18;