@psnext/lscg 0.1.4 → 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.
- package/README.md +87 -9
- package/dist/src/cli-progress.d.ts +7 -0
- package/dist/src/cli-progress.js +59 -0
- package/dist/src/cli.js +14 -6
- package/dist/src/graph/attribution.d.ts +2 -2
- package/dist/src/graph/attribution.js +36 -13
- package/dist/src/graph/repository.d.ts +30 -3
- package/dist/src/graph/repository.js +396 -158
- package/dist/src/graph/repositoryScanWorker.d.ts +21 -0
- package/dist/src/graph/repositoryScanWorker.js +45 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +4 -0
- package/dist/src/mcp/server.js +16 -1
- package/dist/src/parser/treeSitter.js +20 -1
- package/dist/src/scanner/artifactInventory.d.ts +35 -0
- package/dist/src/scanner/artifactInventory.js +139 -0
- package/dist/src/scanner/fingerprint.js +5 -0
- package/dist/src/scanner/javaDependencyPlugin.d.ts +5 -0
- package/dist/src/scanner/javaDependencyPlugin.js +107 -0
- package/dist/src/scanner/javaPlugin.d.ts +5 -0
- package/dist/src/scanner/javaPlugin.js +199 -0
- package/dist/src/scanner/javaScanWorker.d.ts +2 -0
- package/dist/src/scanner/javaScanWorker.js +8 -0
- package/dist/src/scanner/packageParseWorker.d.ts +17 -0
- package/dist/src/scanner/packageParseWorker.js +30 -0
- package/dist/src/scanner/packagePlugin.js +83 -24
- package/dist/src/scanner/parallelScan.d.ts +2 -0
- package/dist/src/scanner/parallelScan.js +32 -0
- package/dist/src/scanner/plugins.d.ts +32 -2
- package/dist/src/scanner/plugins.js +51 -5
- package/dist/src/scanner/pythonPlugin.d.ts +5 -0
- package/dist/src/scanner/pythonPlugin.js +198 -0
- package/dist/src/scanner/pythonScanWorker.d.ts +2 -0
- package/dist/src/scanner/pythonScanWorker.js +8 -0
- package/dist/src/storage/connection.js +63 -0
- package/dist/src/storage/database.d.ts +1 -0
- package/dist/src/storage/database.js +1 -0
- package/dist/src/storage/graph-writes.d.ts +16 -3
- package/dist/src/storage/graph-writes.js +142 -17
- package/dist/src/storage/manifest-inventory.d.ts +23 -0
- package/dist/src/storage/manifest-inventory.js +82 -0
- package/dist/src/storage/queries.d.ts +6 -1
- package/dist/src/storage/queries.js +67 -1
- package/dist/src/storage/schema.d.ts +2 -2
- package/dist/src/storage/schema.js +44 -1
- package/dist/src/types.d.ts +102 -6
- package/dist/src/watch.d.ts +17 -2
- package/dist/src/watch.js +208 -46
- package/package.json +9 -3
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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 (
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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 =
|
|
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;
|
package/dist/src/watch.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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
3
|
export interface WatchCommandOptions {
|
|
4
4
|
root?: string | undefined;
|
|
5
5
|
scope?: StorageScope | 'both' | undefined;
|
|
6
6
|
intervalMs?: number | undefined;
|
|
7
|
+
debounceMs?: number | undefined;
|
|
7
8
|
}
|
|
8
9
|
export interface WatchSummary {
|
|
9
10
|
filesDiscovered: number;
|
|
@@ -11,6 +12,7 @@ export interface WatchSummary {
|
|
|
11
12
|
nodesWritten: number;
|
|
12
13
|
edgesWritten: number;
|
|
13
14
|
skippedCount: number;
|
|
15
|
+
enrichment: EnrichmentSummary;
|
|
14
16
|
}
|
|
15
17
|
export interface WatchEvent {
|
|
16
18
|
event: 'watching' | 'scan' | 'waiting' | 'rescan-triggered' | 'stopped';
|
|
@@ -22,13 +24,26 @@ export interface WatchEvent {
|
|
|
22
24
|
summary?: WatchSummary | undefined;
|
|
23
25
|
scans?: number | undefined;
|
|
24
26
|
}
|
|
27
|
+
export type FileSystemWatchEvent = 'change' | 'rename';
|
|
28
|
+
export type FileSystemWatchListener = (event: FileSystemWatchEvent, filename: string | Buffer | null) => void;
|
|
29
|
+
/** Minimal fs.watch surface so watch behavior can be tested without the filesystem. */
|
|
30
|
+
export interface FileSystemWatcher {
|
|
31
|
+
close(): void;
|
|
32
|
+
on(event: 'error', listener: (error: Error) => void): this;
|
|
33
|
+
}
|
|
34
|
+
export type FileSystemWatcherFactory = (directory: string, options: {
|
|
35
|
+
recursive: boolean;
|
|
36
|
+
}, listener: FileSystemWatchListener) => FileSystemWatcher;
|
|
25
37
|
export interface WatchDependencies {
|
|
26
38
|
scanRepository?: typeof scanRepository | undefined;
|
|
39
|
+
drainEnrichment?: typeof drainRepositoryEnrichment | undefined;
|
|
27
40
|
fingerprint?: ((root: string) => Promise<string> | string) | undefined;
|
|
41
|
+
watcherFactory?: FileSystemWatcherFactory | undefined;
|
|
28
42
|
emit?: ((event: WatchEvent) => void) | undefined;
|
|
29
43
|
wait?: ((ms: number, signal?: AbortSignal) => Promise<void>) | undefined;
|
|
30
44
|
signal?: AbortSignal | undefined;
|
|
31
45
|
mode?: 'watch' | 'scan --watch' | undefined;
|
|
46
|
+
progress?: ProgressReporter | undefined;
|
|
32
47
|
}
|
|
33
48
|
export interface WatchRunResult {
|
|
34
49
|
repository: RepositoryRecord;
|
package/dist/src/watch.js
CHANGED
|
@@ -1,30 +1,115 @@
|
|
|
1
|
+
import { readdirSync, watch as watchFileSystem } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
-
import { repositoryForRoot, scanRepository } from './graph/repository.js';
|
|
4
|
+
import { drainRepositoryEnrichment, repositoryForRoot, scanRepository } from './graph/repository.js';
|
|
3
5
|
import { collectRepositoryFingerprint } from './scanner/fingerprint.js';
|
|
6
|
+
import { discoverSourceFiles } from './scanner/discover.js';
|
|
7
|
+
import { isSupportedSourceFile } from './parser/treeSitter.js';
|
|
4
8
|
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
9
|
+
const DEFAULT_DEBOUNCE_MS = 100;
|
|
10
|
+
const IGNORED_WATCH_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'coverage', '.next', '.turbo', '.cache', '.sling']);
|
|
5
11
|
export async function watchRepository(options = {}, dependencies = {}) {
|
|
6
12
|
const scope = resolveWatchScope(options.scope);
|
|
7
13
|
const repository = repositoryForRoot(options.root);
|
|
8
14
|
const scanFn = dependencies.scanRepository ?? scanRepository;
|
|
15
|
+
const drainEnrichment = dependencies.drainEnrichment ?? drainRepositoryEnrichment;
|
|
9
16
|
const fingerprintFn = dependencies.fingerprint ?? ((root) => collectRepositoryFingerprint(root));
|
|
17
|
+
const watcherFactory = dependencies.watcherFactory ?? defaultWatcherFactory;
|
|
10
18
|
const emit = dependencies.emit ?? defaultEmit;
|
|
11
19
|
const wait = dependencies.wait ?? defaultWait;
|
|
12
20
|
const signal = dependencies.signal;
|
|
13
21
|
const mode = dependencies.mode ?? 'watch';
|
|
22
|
+
const progress = dependencies.progress;
|
|
14
23
|
const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
24
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
15
25
|
let scans = 0;
|
|
16
26
|
let stopped = false;
|
|
17
|
-
let
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
let scanning = false;
|
|
28
|
+
let polling = false;
|
|
29
|
+
let currentFingerprint;
|
|
30
|
+
let followUpQueued = false;
|
|
31
|
+
let pendingEventPaths = new Set();
|
|
32
|
+
let failed = false;
|
|
33
|
+
let wakeWaiting;
|
|
34
|
+
const watchers = [];
|
|
35
|
+
const wake = () => {
|
|
36
|
+
const resolve = wakeWaiting;
|
|
37
|
+
wakeWaiting = undefined;
|
|
38
|
+
resolve?.();
|
|
39
|
+
};
|
|
40
|
+
const queueReconciliation = () => {
|
|
41
|
+
if (scanning) {
|
|
42
|
+
followUpQueued = true;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
pendingEventPaths.add('');
|
|
46
|
+
}
|
|
47
|
+
wake();
|
|
48
|
+
};
|
|
49
|
+
const usePollingFallback = () => {
|
|
50
|
+
if (polling)
|
|
51
|
+
return;
|
|
52
|
+
polling = true;
|
|
53
|
+
currentFingerprint = undefined;
|
|
54
|
+
closeWatchers(watchers);
|
|
55
|
+
reportWatchProgress(progress, { event: 'polling-fallback', mode, repository, scope, detail: 'filesystem watcher unavailable; polling fallback enabled' });
|
|
56
|
+
queueReconciliation();
|
|
57
|
+
};
|
|
58
|
+
const handleWatchEvent = (watchedDirectory, event, filename) => {
|
|
59
|
+
if (event === 'rename' || filename === null) {
|
|
60
|
+
usePollingFallback();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const relativePath = relevantWatchPath(repository.root, watchedDirectory, filename);
|
|
64
|
+
if (!relativePath)
|
|
65
|
+
return;
|
|
66
|
+
pendingEventPaths.add(relativePath);
|
|
67
|
+
if (scanning)
|
|
68
|
+
followUpQueued = true;
|
|
69
|
+
wake();
|
|
70
|
+
};
|
|
71
|
+
const addWatcher = (directory, recursive) => {
|
|
72
|
+
const watcher = watcherFactory(directory, { recursive }, (event, filename) => handleWatchEvent(directory, event, filename));
|
|
73
|
+
watcher.on('error', () => usePollingFallback());
|
|
74
|
+
watchers.push(watcher);
|
|
75
|
+
};
|
|
24
76
|
try {
|
|
77
|
+
try {
|
|
78
|
+
addWatcher(repository.root, true);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
closeWatchers(watchers);
|
|
82
|
+
try {
|
|
83
|
+
for (const directory of watchDirectories(repository.root)) {
|
|
84
|
+
addWatcher(directory, false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
closeWatchers(watchers);
|
|
89
|
+
polling = true;
|
|
90
|
+
reportWatchProgress(progress, { event: 'polling-fallback', mode, repository, scope, detail: 'filesystem watcher unavailable; polling fallback enabled' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
emit({
|
|
94
|
+
event: 'watching',
|
|
95
|
+
mode,
|
|
96
|
+
repository,
|
|
97
|
+
scope
|
|
98
|
+
});
|
|
99
|
+
reportWatchProgress(progress, { event: 'watching', mode, repository, scope, detail: 'watch session started' });
|
|
25
100
|
while (!signal?.aborted) {
|
|
26
|
-
|
|
27
|
-
const
|
|
101
|
+
scanning = true;
|
|
102
|
+
const phase = scans === 0 ? 'initial-scan' : 'rescan';
|
|
103
|
+
reportWatchProgress(progress, { event: phase, mode, repository, scope, detail: scans === 0 ? 'starting initial scan' : 'starting rescan' });
|
|
104
|
+
const scanStartFingerprint = polling ? await fingerprintFn(repository.root) : undefined;
|
|
105
|
+
const structuralSummary = await scanFn({ root: repository.root, scope, progress });
|
|
106
|
+
// A watch owns in-process enrichment. Awaiting it while `scanning` is
|
|
107
|
+
// true serializes durable work with structural replacement; a filesystem
|
|
108
|
+
// event during either phase queues exactly one follow-up structural scan.
|
|
109
|
+
const enrichment = await drainEnrichment({ root: repository.root, scope });
|
|
110
|
+
reportWatchProgress(progress, { event: 'enrichment', mode, repository, scope, detail: `state=${enrichment.state} pending=${enrichment.pending} complete=${enrichment.complete} failed=${enrichment.failed}` });
|
|
111
|
+
const summary = { ...structuralSummary, enrichment };
|
|
112
|
+
scanning = false;
|
|
28
113
|
scans += 1;
|
|
29
114
|
emit({
|
|
30
115
|
event: 'scan',
|
|
@@ -34,44 +119,60 @@ export async function watchRepository(options = {}, dependencies = {}) {
|
|
|
34
119
|
scope,
|
|
35
120
|
summary: summarizeScan(summary)
|
|
36
121
|
});
|
|
37
|
-
|
|
38
|
-
|
|
122
|
+
if (polling) {
|
|
123
|
+
const scanEndFingerprint = await fingerprintFn(repository.root);
|
|
124
|
+
const changedDuringScan = scanStartFingerprint !== undefined && scanEndFingerprint !== scanStartFingerprint;
|
|
125
|
+
currentFingerprint = scanEndFingerprint;
|
|
126
|
+
if (changedDuringScan)
|
|
127
|
+
followUpQueued = true;
|
|
128
|
+
}
|
|
39
129
|
if (signal?.aborted)
|
|
40
130
|
break;
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
reason: 'follow-up',
|
|
46
|
-
repository,
|
|
47
|
-
scope
|
|
48
|
-
});
|
|
131
|
+
if (followUpQueued) {
|
|
132
|
+
followUpQueued = false;
|
|
133
|
+
pendingEventPaths = new Set();
|
|
134
|
+
emitRescanTriggered('follow-up', mode, repository, scope, emit, progress);
|
|
49
135
|
continue;
|
|
50
136
|
}
|
|
51
|
-
emit({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const nextFingerprint = await fingerprintFn(repository.root);
|
|
60
|
-
if (nextFingerprint !== currentFingerprint) {
|
|
137
|
+
emit({ event: 'waiting', mode, repository, scope });
|
|
138
|
+
reportWatchProgress(progress, { event: 'waiting', mode, repository, scope, detail: polling ? 'waiting for polling interval' : 'waiting for file changes' });
|
|
139
|
+
if (polling) {
|
|
140
|
+
while (!signal?.aborted) {
|
|
141
|
+
await wait(intervalMs, signal);
|
|
142
|
+
const nextFingerprint = await fingerprintFn(repository.root);
|
|
143
|
+
if (nextFingerprint === currentFingerprint)
|
|
144
|
+
continue;
|
|
61
145
|
currentFingerprint = nextFingerprint;
|
|
62
|
-
emit
|
|
63
|
-
event: 'rescan-triggered',
|
|
64
|
-
mode,
|
|
65
|
-
reason: 'file-change',
|
|
66
|
-
repository,
|
|
67
|
-
scope
|
|
68
|
-
});
|
|
146
|
+
emitRescanTriggered('file-change', mode, repository, scope, emit, progress);
|
|
69
147
|
break;
|
|
70
148
|
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (pendingEventPaths.size === 0) {
|
|
152
|
+
await waitForWatchEvent(signal, (resolve) => {
|
|
153
|
+
wakeWaiting = resolve;
|
|
154
|
+
});
|
|
71
155
|
}
|
|
156
|
+
if (signal?.aborted)
|
|
157
|
+
break;
|
|
158
|
+
if (polling)
|
|
159
|
+
continue;
|
|
160
|
+
if (pendingEventPaths.size === 0)
|
|
161
|
+
continue;
|
|
162
|
+
await wait(debounceMs, signal);
|
|
163
|
+
if (signal?.aborted)
|
|
164
|
+
break;
|
|
165
|
+
pendingEventPaths = new Set();
|
|
166
|
+
emitRescanTriggered('file-change', mode, repository, scope, emit, progress);
|
|
72
167
|
}
|
|
73
168
|
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
failed = true;
|
|
171
|
+
reportWatchProgress(progress, { event: 'failed', mode, repository, scope, detail: error instanceof Error ? error.message : String(error) });
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
74
174
|
finally {
|
|
175
|
+
closeWatchers(watchers);
|
|
75
176
|
stopped = true;
|
|
76
177
|
emit({
|
|
77
178
|
event: 'stopped',
|
|
@@ -80,28 +181,90 @@ export async function watchRepository(options = {}, dependencies = {}) {
|
|
|
80
181
|
scope,
|
|
81
182
|
scans
|
|
82
183
|
});
|
|
184
|
+
if (!failed)
|
|
185
|
+
reportWatchProgress(progress, { event: 'stopped', mode, repository, scope, detail: `watch session stopped after ${scans} scan(s)` });
|
|
83
186
|
}
|
|
84
|
-
return {
|
|
85
|
-
repository,
|
|
86
|
-
scope,
|
|
87
|
-
scans,
|
|
88
|
-
stopped
|
|
89
|
-
};
|
|
187
|
+
return { repository, scope, scans, stopped };
|
|
90
188
|
}
|
|
91
189
|
function resolveWatchScope(scope) {
|
|
92
190
|
if (scope === undefined || scope === 'repo')
|
|
93
191
|
return 'repo';
|
|
94
192
|
throw new Error('watch mode supports repo scope only');
|
|
95
193
|
}
|
|
194
|
+
function watchDirectories(root) {
|
|
195
|
+
const directories = new Set([root]);
|
|
196
|
+
for (const relativePath of discoverSourceFiles(root)) {
|
|
197
|
+
directories.add(path.dirname(path.join(root, relativePath)));
|
|
198
|
+
}
|
|
199
|
+
// Immediate module directories contain the supported Maven/Gradle manifests
|
|
200
|
+
// and must also be watched when a module has no source files yet.
|
|
201
|
+
try {
|
|
202
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
203
|
+
if (entry.isDirectory() && !entry.isSymbolicLink() && !IGNORED_WATCH_DIRECTORIES.has(entry.name))
|
|
204
|
+
directories.add(path.join(root, entry.name));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch { /* scan will report traversal degradation */ }
|
|
208
|
+
return [...directories].sort();
|
|
209
|
+
}
|
|
210
|
+
function relevantWatchPath(root, watchedDirectory, filename) {
|
|
211
|
+
const filenameText = Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
212
|
+
const absolutePath = path.resolve(watchedDirectory, filenameText);
|
|
213
|
+
const relativePath = path.relative(root, absolutePath);
|
|
214
|
+
if (!relativePath || path.isAbsolute(relativePath) || relativePath === '..' || relativePath.startsWith(`..${path.sep}`))
|
|
215
|
+
return undefined;
|
|
216
|
+
const pathSegments = relativePath.split(path.sep);
|
|
217
|
+
if (pathSegments.some((segment) => IGNORED_WATCH_DIRECTORIES.has(segment)))
|
|
218
|
+
return undefined;
|
|
219
|
+
const basename = path.basename(relativePath);
|
|
220
|
+
const depth = pathSegments.length;
|
|
221
|
+
const supportedManifest = depth <= 2 && (basename === 'pom.xml' || basename === 'build.gradle' || basename === 'build.gradle.kts');
|
|
222
|
+
if (basename !== 'package.json' && !isSupportedSourceFile(absolutePath) && !supportedManifest)
|
|
223
|
+
return undefined;
|
|
224
|
+
return relativePath;
|
|
225
|
+
}
|
|
226
|
+
function closeWatchers(watchers) {
|
|
227
|
+
while (watchers.length > 0) {
|
|
228
|
+
watchers.pop()?.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function waitForWatchEvent(signal, setWake) {
|
|
232
|
+
return new Promise((resolve) => {
|
|
233
|
+
const done = () => {
|
|
234
|
+
signal?.removeEventListener('abort', done);
|
|
235
|
+
resolve();
|
|
236
|
+
};
|
|
237
|
+
if (signal?.aborted) {
|
|
238
|
+
done();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
setWake(done);
|
|
242
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function emitRescanTriggered(reason, mode, repository, scope, emit, progress) {
|
|
246
|
+
emit({ event: 'rescan-triggered', mode, reason, repository, scope });
|
|
247
|
+
reportWatchProgress(progress, { event: reason === 'follow-up' ? 'follow-up' : 'coalesced-change', mode, repository, scope, detail: reason === 'follow-up' ? 'change queued during scan; running follow-up' : 'coalesced file changes; running one rescan' });
|
|
248
|
+
}
|
|
96
249
|
function summarizeScan(summary) {
|
|
97
250
|
return {
|
|
98
251
|
filesDiscovered: summary.filesDiscovered,
|
|
99
252
|
filesScanned: summary.filesScanned,
|
|
100
253
|
nodesWritten: summary.nodesWritten,
|
|
101
254
|
edgesWritten: summary.edgesWritten,
|
|
102
|
-
skippedCount: summary.skipped.length
|
|
255
|
+
skippedCount: summary.skipped.length,
|
|
256
|
+
enrichment: summary.enrichment
|
|
103
257
|
};
|
|
104
258
|
}
|
|
259
|
+
function reportWatchProgress(progress, event) {
|
|
260
|
+
try {
|
|
261
|
+
progress?.({ kind: 'watch', ...event });
|
|
262
|
+
}
|
|
263
|
+
catch { /* progress is observational */ }
|
|
264
|
+
}
|
|
265
|
+
function defaultWatcherFactory(directory, options, listener) {
|
|
266
|
+
return watchFileSystem(directory, options, (event, filename) => listener(event, filename));
|
|
267
|
+
}
|
|
105
268
|
function defaultEmit(event) {
|
|
106
269
|
console.log(JSON.stringify(event));
|
|
107
270
|
}
|
|
@@ -110,9 +273,8 @@ async function defaultWait(ms, signal) {
|
|
|
110
273
|
await sleep(ms, undefined, signal ? { signal } : undefined);
|
|
111
274
|
}
|
|
112
275
|
catch (error) {
|
|
113
|
-
if (!signal?.aborted)
|
|
276
|
+
if (!signal?.aborted)
|
|
114
277
|
throw error;
|
|
115
|
-
}
|
|
116
278
|
}
|
|
117
279
|
}
|
|
118
280
|
//# sourceMappingURL=watch.js.map
|