@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
package/README.md
CHANGED
|
@@ -119,9 +119,12 @@ prefix and substring matches; an ambiguous match returns ranked candidates
|
|
|
119
119
|
instead of merging unrelated symbols. Use `--kind` and repository-relative
|
|
120
120
|
`--file` to disambiguate. Results are references by default; `--excerpts` opts
|
|
121
121
|
into bounded source text with `--excerpt-lines` and `--excerpt-bytes` limits.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
Graph-reading commands never start a scan. Their Freshness Report reflects the
|
|
123
|
+
stored graph state: `fresh` after a successful explicit scan, `stale` when no
|
|
124
|
+
compatible graph exists or a known invalidation remains, and `degraded` when a
|
|
125
|
+
retained failed-file or plugin contribution is being served. Run `scan` or keep
|
|
126
|
+
`watch` active after source changes; a stored `fresh` result is not a live
|
|
127
|
+
filesystem content-verification guarantee.
|
|
125
128
|
|
|
126
129
|
Impact contains only syntax-grounded `calls` edges (with caller/callee
|
|
127
130
|
direction), and dependencies contain `imports`/`exports`; parser confidence
|
|
@@ -130,10 +133,70 @@ Substring fallback is intentionally reported as a bounded capability warning.
|
|
|
130
133
|
Use `rg` or `grep` for literal discovery when a term is not indexed—`context`
|
|
131
134
|
does not replace arbitrary text search or claim complete semantic resolution.
|
|
132
135
|
|
|
136
|
+
`lscg scan` performs an Explicit Scan. After the first scan, its default mode
|
|
137
|
+
is incremental: it retains unchanged file graphs and reprocesses files whose
|
|
138
|
+
path, size, or mtime metadata changed. Use `lscg scan --full` when you need a
|
|
139
|
+
complete rebuild and content verification (including same-size edits with
|
|
140
|
+
restored mtimes).
|
|
141
|
+
|
|
133
142
|
`lscg watch` keeps the selected repository current in the foreground. It runs
|
|
134
143
|
one scan immediately, then watches scanner-relevant files and prints compact
|
|
135
|
-
status updates between rescans.
|
|
136
|
-
scan
|
|
144
|
+
status updates between rescans. `lscg scan --watch` provides the same Watch
|
|
145
|
+
Session from the scan command. Use `lscg scan` when you want the one-shot scan
|
|
146
|
+
output.
|
|
147
|
+
|
|
148
|
+
Pass `--progress` to `scan`, `watch`, or `scan --watch` for line-oriented,
|
|
149
|
+
human-readable lifecycle messages on stderr while the existing JSON summary or
|
|
150
|
+
Watch Session events remain on stdout. For example:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm start -- scan --progress >summary.json
|
|
154
|
+
npm start -- watch --progress >watch-events.jsonl
|
|
155
|
+
npm start -- scan --watch --progress >watch-events.jsonl
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Progress labels discovery/planning, plugin work, file processing,
|
|
159
|
+
reconciliation, and optional enrichment. A normal scan may report structural
|
|
160
|
+
completion with enrichment deferred or degraded; a Full Scan reports retries
|
|
161
|
+
and only reports stabilized completion after its enrichment loop succeeds.
|
|
162
|
+
Progress is informational rather than a stable machine-readable protocol, and
|
|
163
|
+
it does not provide cancellation or animated progress bars.
|
|
164
|
+
|
|
165
|
+
## Scan/watch benchmark
|
|
166
|
+
|
|
167
|
+
Run the non-blocking benchmark with:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm run benchmark:scan-watch
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The versioned JSON report separates two cold timings for this lscg checkout:
|
|
174
|
+
|
|
175
|
+
- `coldNormalStructuralReady`: a normal scan's return after its structural
|
|
176
|
+
graph is persisted, before optional enrichment completes.
|
|
177
|
+
- `coldFullEnrichmentComplete`: a cold `scan --full` completion, including its
|
|
178
|
+
required enrichment drain.
|
|
179
|
+
|
|
180
|
+
Every cold sample uses an isolated copy with no pre-existing graph database;
|
|
181
|
+
source and Git metadata are copied so the normal and full scans exercise the
|
|
182
|
+
same source graph and optional attribution path without changing this checkout.
|
|
183
|
+
The report records SHA-256 fingerprints plus structural node/edge counts before
|
|
184
|
+
normal-scan enrichment and after that scan's pending enrichment work drains.
|
|
185
|
+
`structuralEquivalence` must be `true`: user nodes, `attributed_to` edges, and
|
|
186
|
+
per-node attribution are
|
|
187
|
+
intentionally excluded because they are optional enrichment rather than source
|
|
188
|
+
structure.
|
|
189
|
+
|
|
190
|
+
`coldNormalStructuralReadyTarget` reports the 3.43-second baseline, the
|
|
191
|
+
2.06-second median target (the required 40% reduction), the measured p50, and
|
|
192
|
+
whether that run met the target. Timing values and target status are benchmark
|
|
193
|
+
results for the documented machine/run, not unit-test thresholds. Use multiple
|
|
194
|
+
cold samples for review (the default is 10); pass `-- --repetitions 5` to change
|
|
195
|
+
the sample count.
|
|
196
|
+
|
|
197
|
+
The report also retains deterministic work counters and informational p50/p95
|
|
198
|
+
timings for the isolated representative fixture's initial full scan, no-change
|
|
199
|
+
incremental scan, and one-file Watch Session update.
|
|
137
200
|
|
|
138
201
|
When the repository has git history, `lscg scan` also records contributor user
|
|
139
202
|
nodes keyed by email and tracks the most recent contributor for each discovered
|
|
@@ -203,7 +266,22 @@ listed in `plugins`.
|
|
|
203
266
|
|
|
204
267
|
## Current parser support
|
|
205
268
|
|
|
206
|
-
The
|
|
207
|
-
Tree-sitter.
|
|
208
|
-
|
|
209
|
-
|
|
269
|
+
The scanner supports JavaScript, JSX, TypeScript, TSX, Python, and Java via
|
|
270
|
+
Tree-sitter. Python is handled by the bundled `PythonScannerPlugin`, and Java
|
|
271
|
+
by `JavaScannerPlugin` using the pinned native `tree-sitter-java@0.21.0`
|
|
272
|
+
grammar. Java records syntax-grounded classes, interfaces, enums, records,
|
|
273
|
+
annotation types, methods, constructors, fields, imports, method invocations,
|
|
274
|
+
containment, and explicit-public export approximations. Java calls/imports are
|
|
275
|
+
occurrences (no type, overload, constructor, or cross-file resolution), and
|
|
276
|
+
inherited/module APIs are not modeled. A Java parse error retains the prior
|
|
277
|
+
file contribution and marks freshness degraded until the file recovers.
|
|
278
|
+
|
|
279
|
+
Repositories with root or immediate-module `pom.xml`, `build.gradle`, or
|
|
280
|
+
`build.gradle.kts` files also receive `java-dependency` `package` nodes from
|
|
281
|
+
`JavaDependencyPlugin`. Manifest reading is bounded and non-executing; only
|
|
282
|
+
literal Maven coordinates/properties and literal Gradle dependency calls are
|
|
283
|
+
modeled. Dependencies remain independent from Java imports: no heuristic
|
|
284
|
+
coordinate matching or dependency-to-import edges are emitted. Native
|
|
285
|
+
Tree-sitter install scripts must remain allowlisted for clean installs.
|
|
286
|
+
Cross-file symbol resolution remains a language-specific semantic-enricher
|
|
287
|
+
concern.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ProgressReporter } from './types.js';
|
|
2
|
+
export interface ProgressWriter {
|
|
3
|
+
(text: string): void;
|
|
4
|
+
}
|
|
5
|
+
/** Create a deterministic, line-oriented renderer for the stderr channel. */
|
|
6
|
+
export declare function createProgressReporter(write?: ProgressWriter): ProgressReporter;
|
|
7
|
+
//# sourceMappingURL=cli-progress.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Create a deterministic, line-oriented renderer for the stderr channel. */
|
|
2
|
+
export function createProgressReporter(write = (text) => process.stderr.write(`${text}\n`)) {
|
|
3
|
+
const skippedByAttempt = new Map();
|
|
4
|
+
return (event) => {
|
|
5
|
+
const line = event.kind === 'scan'
|
|
6
|
+
? renderScan(event, skippedByAttempt)
|
|
7
|
+
: renderWatch(event);
|
|
8
|
+
if (line)
|
|
9
|
+
write(line);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function renderScan(event, skippedByAttempt) {
|
|
13
|
+
const prefix = `[${event.operation} scan scope=${event.scope}]`;
|
|
14
|
+
if (event.phase === 'discovery' && event.status === 'started')
|
|
15
|
+
return `${prefix} discovery/planning: discovering files`;
|
|
16
|
+
if (event.phase === 'discovery' && event.status === 'completed') {
|
|
17
|
+
const c = event.counts ?? {};
|
|
18
|
+
return `${prefix} discovery/planning: discovered=${c.discovered ?? 0} selected=${c.selected ?? 0} unchanged=${c.unchanged ?? 0} deleted=${c.deleted ?? 0}`;
|
|
19
|
+
}
|
|
20
|
+
if (event.phase === 'plugins' && event.status === 'started')
|
|
21
|
+
return `${prefix} plugin work: starting plugins=${event.counts?.plugins ?? 0}`;
|
|
22
|
+
if (event.phase === 'plugins') {
|
|
23
|
+
const c = event.counts ?? {};
|
|
24
|
+
const suffix = event.detail ? ` (${event.detail})` : '';
|
|
25
|
+
return `${prefix} plugin work: plugins=${c.plugins ?? 0} failed=${c.failedPlugins ?? 0}${suffix}`;
|
|
26
|
+
}
|
|
27
|
+
if (event.phase === 'files' && event.status === 'started')
|
|
28
|
+
return `${prefix} file processing: selected=${event.counts?.selected ?? 0}`;
|
|
29
|
+
if (event.phase === 'files' && event.status === 'processed')
|
|
30
|
+
return `${prefix} file processing: processed=${event.counts?.processed ?? 0} skipped=${event.counts?.skipped ?? 0}`;
|
|
31
|
+
if (event.phase === 'files' && event.status === 'skipped') {
|
|
32
|
+
const key = `${event.operation}:${event.attempt ?? 1}`;
|
|
33
|
+
const count = (skippedByAttempt.get(key) ?? 0) + 1;
|
|
34
|
+
skippedByAttempt.set(key, count);
|
|
35
|
+
if (count <= 3)
|
|
36
|
+
return `${prefix} warning: skipped ${event.path ?? '<unknown>'}${event.detail ? ` (${event.detail})` : ''}`;
|
|
37
|
+
if (count === 4)
|
|
38
|
+
return `${prefix} warning: additional skipped files omitted (showing first 3)`;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (event.phase === 'reconciliation' && event.status === 'started')
|
|
42
|
+
return `${prefix} reconciliation: starting deleted=${event.counts?.deleted ?? 0}`;
|
|
43
|
+
if (event.phase === 'reconciliation' && event.status === 'completed')
|
|
44
|
+
return `${prefix} reconciliation: complete processed=${event.counts?.processed ?? 0} skipped=${event.counts?.skipped ?? 0}`;
|
|
45
|
+
if (event.phase === 'enrichment' && event.status === 'started')
|
|
46
|
+
return `${prefix} enrichment: starting`;
|
|
47
|
+
if (event.phase === 'enrichment') {
|
|
48
|
+
const c = event.counts ?? {};
|
|
49
|
+
return `${prefix} enrichment: ${event.status} pending=${c.pending ?? 0} complete=${c.complete ?? 0} failed=${c.failed ?? 0}${event.detail ? ` (${event.detail})` : ''}`;
|
|
50
|
+
}
|
|
51
|
+
if (event.phase === 'terminal')
|
|
52
|
+
return `${prefix} ${event.status === 'failed' ? 'ERROR' : event.status === 'degraded' ? 'warning' : 'complete'}${event.detail ? `: ${event.detail}` : ''}`;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function renderWatch(event) {
|
|
56
|
+
const prefix = `[watch mode=${event.mode} scope=${event.scope}]`;
|
|
57
|
+
return `${prefix} ${event.event}${event.detail ? `: ${event.detail}` : ''}`;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=cli-progress.js.map
|
package/dist/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { startMcpServer } from './mcp/server.js';
|
|
2
2
|
import { buildViewModel, loadViewSnapshot, renderSvgMarkup, viewGraph } from './view/index.js';
|
|
3
3
|
import { watchRepository } from './watch.js';
|
|
4
|
+
import { createProgressReporter } from './cli-progress.js';
|
|
4
5
|
import { graphStatus, initGraph, callGraph, contextGraph, listEdges, listNodes, listNodeText, neighbors, runReadOnlySql, scanRepository } from './graph/repository.js';
|
|
5
6
|
const operationalCommands = new Set([
|
|
6
7
|
'init', 'scan', 'watch', 'status', 'nodes', 'edges', 'neighbors',
|
|
@@ -13,19 +14,22 @@ export async function runCli(argv) {
|
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
16
|
const options = parseOptions(rest);
|
|
17
|
+
const progress = options.progress === true ? createProgressReporter() : undefined;
|
|
16
18
|
switch (command) {
|
|
17
19
|
case 'init':
|
|
18
20
|
printJson(initGraph({ root: options.root, scope: options.scope ?? 'repo' }));
|
|
19
21
|
return;
|
|
20
22
|
case 'scan':
|
|
23
|
+
if (options.watch && options.full)
|
|
24
|
+
throw new Error('scan --full cannot be combined with --watch');
|
|
21
25
|
if (options.watch) {
|
|
22
|
-
await watchRepository({ root: options.root, scope: options.scope ?? 'repo' }, { mode: 'scan --watch' });
|
|
26
|
+
await watchRepository({ root: options.root, scope: options.scope ?? 'repo' }, { mode: 'scan --watch', progress });
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
25
|
-
printJson(await scanRepository({ root: options.root, scope: options.scope ?? 'repo' }));
|
|
29
|
+
printJson(await scanRepository({ root: options.root, scope: options.scope ?? 'repo', full: options.full === true, progress }));
|
|
26
30
|
return;
|
|
27
31
|
case 'watch':
|
|
28
|
-
await watchRepository({ root: options.root, scope: options.scope ?? 'repo' }, { mode: 'watch' });
|
|
32
|
+
await watchRepository({ root: options.root, scope: options.scope ?? 'repo' }, { mode: 'watch', progress });
|
|
29
33
|
return;
|
|
30
34
|
case 'status':
|
|
31
35
|
printJson(graphStatus({ root: options.root, scope: options.scope ?? 'repo' }));
|
|
@@ -238,7 +242,7 @@ function parseOptions(args) {
|
|
|
238
242
|
continue;
|
|
239
243
|
}
|
|
240
244
|
const next = args[index + 1];
|
|
241
|
-
const booleanOption = key === 'watch' || key === 'attachHome' || key === 'excerpts' || key === 'help';
|
|
245
|
+
const booleanOption = key === 'watch' || key === 'full' || key === 'attachHome' || key === 'excerpts' || key === 'help' || key === 'progress';
|
|
242
246
|
if (!booleanOption && next && !next.startsWith('--')) {
|
|
243
247
|
options[key] = coerce(next);
|
|
244
248
|
index += 1;
|
|
@@ -286,8 +290,8 @@ function validateContextOptions(options, args) {
|
|
|
286
290
|
function commandHelpText(command) {
|
|
287
291
|
const help = {
|
|
288
292
|
init: `Usage: lscg init [options]\n\nCreate graph storage for the selected scope.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo|home|both Storage scope to initialize (default: repo).\n\nExamples:\n lscg init --scope both`,
|
|
289
|
-
scan: `Usage: lscg scan [options]\n\nParse repository files and write discovered graph data.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo|home|both Storage scope to scan (default: repo).\n --watch Watch and rescan after the initial scan.\n\nExamples:\n lscg scan --scope both\n lscg scan --watch`,
|
|
290
|
-
watch: `Usage: lscg watch [options]\n\nKeep the repository current by rescanning on file changes.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo Repository scope to watch (default: repo).\n\nExamples:\n lscg watch`,
|
|
293
|
+
scan: `Usage: lscg scan [options]\n\nParse repository files and write discovered graph data.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo|home|both Storage scope to scan (default: repo).\n --full Rebuild and verify every discovered source file.\n --watch Watch and rescan after the initial scan.\n --progress Write human-readable lifecycle progress to stderr.\n\nExamples:\n lscg scan --scope both\n lscg scan --full\n lscg scan --watch`,
|
|
294
|
+
watch: `Usage: lscg watch [options]\n\nKeep the repository current by rescanning on file changes.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo Repository scope to watch (default: repo).\n --progress Write human-readable lifecycle progress to stderr.\n\nExamples:\n lscg watch`,
|
|
291
295
|
status: `Usage: lscg status [options]\n\nShow graph counts and database paths.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo|home|both Storage scope to inspect (default: repo).\n\nExamples:\n lscg status --scope repo`,
|
|
292
296
|
nodes: `Usage: lscg nodes [term] [options]\n\nList graph nodes, optionally filtered by a case-insensitive name term.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo|home|both Storage scope to inspect (default: repo).\n --kind node-kind Filter by node kind.\n --limit n Maximum number of nodes (default: 50).\n --output json|text Output format (default: json).\n -o value Alias for --output.\n\nExamples:\n lscg nodes --kind user --limit 20\n lscg nodes --output text`,
|
|
293
297
|
edges: `Usage: lscg edges [options]\n\nList graph edges.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo|home|both Storage scope to inspect (default: repo).\n --kind edge-kind Filter by edge kind.\n --limit n Maximum number of edges (default: 50).\n --output json|text Output format (default: json).\n -o value Alias for --output.\n\nExamples:\n lscg edges --kind calls --limit 20\n lscg edges --output text`,
|
|
@@ -479,10 +483,13 @@ Command options:
|
|
|
479
483
|
scan:
|
|
480
484
|
--root path Repository root (default: current directory).
|
|
481
485
|
--scope repo|home|both Storage scope to scan (default: repo).
|
|
486
|
+
--full Rebuild and verify every discovered source file.
|
|
482
487
|
--watch Watch and rescan after the initial scan.
|
|
488
|
+
--progress Write human-readable lifecycle progress to stderr.
|
|
483
489
|
watch:
|
|
484
490
|
--root path Repository root (default: current directory).
|
|
485
491
|
--scope repo Repository scope to watch (default: repo).
|
|
492
|
+
--progress Write human-readable lifecycle progress to stderr.
|
|
486
493
|
status:
|
|
487
494
|
--root path Repository root (default: current directory).
|
|
488
495
|
--scope repo|home|both Storage scope to inspect (default: repo).
|
|
@@ -570,6 +577,7 @@ Notes:
|
|
|
570
577
|
- lscg view opens an interactive HTML page by default.
|
|
571
578
|
- Pass --output to write a static SVG snapshot instead of opening the browser.
|
|
572
579
|
- lscg watch supports repo scope only.
|
|
580
|
+
- --progress is opt-in human-readable diagnostics on stderr; stdout JSON is unchanged.
|
|
573
581
|
|
|
574
582
|
Storage:
|
|
575
583
|
repo: <root>/.sling/graph.sqlite
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AttributionOutcome, GraphNode } from '../types.js';
|
|
2
2
|
export declare function buildFileAttribution({ root, relativePath, source, nodes }: {
|
|
3
3
|
root: string;
|
|
4
4
|
relativePath: string;
|
|
5
5
|
source: string;
|
|
6
6
|
nodes: GraphNode[];
|
|
7
|
-
}):
|
|
7
|
+
}): AttributionOutcome;
|
|
8
8
|
//# sourceMappingURL=attribution.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
export function buildFileAttribution({ root, relativePath, source, nodes }) {
|
|
3
|
-
const
|
|
4
|
-
if (
|
|
5
|
-
return
|
|
3
|
+
const blameResult = blameFile(root, relativePath);
|
|
4
|
+
if (blameResult.status !== 'complete')
|
|
5
|
+
return blameResult;
|
|
6
6
|
const lineStarts = buildLineStarts(source);
|
|
7
7
|
const contributorEmails = new Map();
|
|
8
8
|
const nodeAttributions = nodes.flatMap((node) => {
|
|
@@ -10,7 +10,7 @@ export function buildFileAttribution({ root, relativePath, source, nodes }) {
|
|
|
10
10
|
const uniqueEmails = new Set();
|
|
11
11
|
let latest = null;
|
|
12
12
|
for (let lineNumber = span.startLine; lineNumber <= span.endLine; lineNumber += 1) {
|
|
13
|
-
const blame = blameLines[lineNumber];
|
|
13
|
+
const blame = blameResult.blameLines[lineNumber];
|
|
14
14
|
if (!blame)
|
|
15
15
|
continue;
|
|
16
16
|
uniqueEmails.add(blame.email);
|
|
@@ -25,19 +25,37 @@ export function buildFileAttribution({ root, relativePath, source, nodes }) {
|
|
|
25
25
|
lastModifiedEmail: latest?.email ?? null
|
|
26
26
|
}];
|
|
27
27
|
});
|
|
28
|
+
if (contributorEmails.size === 0)
|
|
29
|
+
return { status: 'unavailable', reason: 'not_applicable' };
|
|
28
30
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
status: 'complete',
|
|
32
|
+
attribution: {
|
|
33
|
+
contributorEmails: [...contributorEmails.values()].sort((left, right) => left.email.localeCompare(right.email)),
|
|
34
|
+
nodeAttributions
|
|
35
|
+
}
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
function blameFile(root, relativePath) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
let result;
|
|
40
|
+
try {
|
|
41
|
+
result = spawnSync('git', ['-C', root, 'blame', '--follow', '--line-porcelain', '--', relativePath], {
|
|
42
|
+
encoding: 'utf8'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return { status: 'failed', diagnostic: `git blame failed: ${diagnosticFor(error)}` };
|
|
39
47
|
}
|
|
40
|
-
|
|
48
|
+
if (result.error)
|
|
49
|
+
return { status: 'failed', diagnostic: `git blame failed: ${diagnosticFor(result.error)}` };
|
|
50
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : result.stdout.toString('utf8');
|
|
51
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : result.stderr.toString('utf8');
|
|
52
|
+
if (result.status !== 0) {
|
|
53
|
+
const detail = stderr.trim() || `exit status ${result.status ?? 'unknown'}`;
|
|
54
|
+
return { status: 'failed', diagnostic: `git blame failed: ${detail}` };
|
|
55
|
+
}
|
|
56
|
+
if (!stdout.trim())
|
|
57
|
+
return { status: 'unavailable', reason: 'not_applicable' };
|
|
58
|
+
const lines = stdout.split(/\r?\n/);
|
|
41
59
|
const blameLines = [];
|
|
42
60
|
let currentEmail = null;
|
|
43
61
|
let currentTimestamp = 0;
|
|
@@ -63,7 +81,12 @@ function blameFile(root, relativePath) {
|
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
}
|
|
66
|
-
return blameLines.length > 0
|
|
84
|
+
return blameLines.length > 0
|
|
85
|
+
? { status: 'complete', blameLines }
|
|
86
|
+
: { status: 'unavailable', reason: 'unavailable' };
|
|
87
|
+
}
|
|
88
|
+
function diagnosticFor(error) {
|
|
89
|
+
return error instanceof Error ? error.message : String(error);
|
|
67
90
|
}
|
|
68
91
|
function isHeaderLine(line) {
|
|
69
92
|
return /^[0-9a-f]{7,40} \d+ \d+(?: \d+)?$/.test(line);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GraphEdgeRow, GraphNodeRow, GraphNodeTextRow, NeighborRow, CallGraphMatch, RepositoryRecord, ScanSummary, StatusResult, StorageScope, ContextGraphResult } from '../types.js';
|
|
1
|
+
import type { EnrichmentRunner, EnrichmentSummary, GraphEdgeRow, GraphNodeRow, GraphNodeTextRow, NeighborRow, CallGraphMatch, RepositoryRecord, ScanSummary, StatusResult, StorageScope, ContextGraphResult, FreshnessReport, ProgressReporter } from '../types.js';
|
|
2
2
|
import { type PluginResourceLimits, type ScannerPlugin } from '../scanner/plugins.js';
|
|
3
3
|
export declare function repositoryForRoot(root?: string | undefined): RepositoryRecord;
|
|
4
4
|
export declare function initGraph({ root, scope }?: {
|
|
@@ -11,12 +11,33 @@ export declare function initGraph({ root, scope }?: {
|
|
|
11
11
|
databasePath: string;
|
|
12
12
|
}>;
|
|
13
13
|
};
|
|
14
|
-
export
|
|
14
|
+
export interface ScanRepositoryOptions {
|
|
15
15
|
root?: string | undefined;
|
|
16
16
|
scope?: StorageScope | 'both' | undefined;
|
|
17
17
|
plugins?: readonly ScannerPlugin[] | undefined;
|
|
18
18
|
pluginLimits?: Partial<PluginResourceLimits> | undefined;
|
|
19
|
-
|
|
19
|
+
full?: boolean | undefined;
|
|
20
|
+
enrichmentRunner?: EnrichmentRunner | undefined;
|
|
21
|
+
progress?: ProgressReporter | undefined;
|
|
22
|
+
/** Internal attempt label used by the Full Scan stabilization loop. */
|
|
23
|
+
attempt?: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A full scan must not report success with hash-incompatible enrichment work.
|
|
27
|
+
* Rebuild and drain again when a source changes while attribution is running.
|
|
28
|
+
*/
|
|
29
|
+
export declare function scanRepository(options?: ScanRepositoryOptions): Promise<ScanSummary>;
|
|
30
|
+
export declare const defaultEnrichmentRunner: EnrichmentRunner;
|
|
31
|
+
export interface DrainRepositoryEnrichmentOptions {
|
|
32
|
+
root?: string | undefined;
|
|
33
|
+
scope?: StorageScope | 'both' | undefined;
|
|
34
|
+
enrichmentRunner?: EnrichmentRunner | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Drains persisted, hash-compatible attribution work. This is deliberately
|
|
38
|
+
* explicit: one-shot structural scans persist work but never start it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function drainRepositoryEnrichment({ root, scope, enrichmentRunner }?: DrainRepositoryEnrichmentOptions): Promise<EnrichmentSummary>;
|
|
20
41
|
export declare function graphStatus({ root, scope }?: {
|
|
21
42
|
root?: string | undefined;
|
|
22
43
|
scope?: StorageScope | 'both' | undefined;
|
|
@@ -31,6 +52,7 @@ export declare function listNodes({ root, scope, kind, limit }?: {
|
|
|
31
52
|
limit?: number | undefined;
|
|
32
53
|
}): Array<{
|
|
33
54
|
scope: StorageScope;
|
|
55
|
+
freshness: FreshnessReport;
|
|
34
56
|
} & GraphNodeRow>;
|
|
35
57
|
export declare function listNodeText({ root, scope, kind, term, limit }?: {
|
|
36
58
|
root?: string | undefined;
|
|
@@ -41,6 +63,7 @@ export declare function listNodeText({ root, scope, kind, term, limit }?: {
|
|
|
41
63
|
}): Array<{
|
|
42
64
|
scope: StorageScope;
|
|
43
65
|
root: string;
|
|
66
|
+
freshness: FreshnessReport;
|
|
44
67
|
} & GraphNodeTextRow>;
|
|
45
68
|
export declare function listEdges({ root, scope, kind, limit }?: {
|
|
46
69
|
root?: string | undefined;
|
|
@@ -49,6 +72,7 @@ export declare function listEdges({ root, scope, kind, limit }?: {
|
|
|
49
72
|
limit?: number | undefined;
|
|
50
73
|
}): Array<{
|
|
51
74
|
scope: StorageScope;
|
|
75
|
+
freshness: FreshnessReport;
|
|
52
76
|
} & GraphEdgeRow>;
|
|
53
77
|
export declare function neighbors({ root, scope, nodeId, depth, limit }?: {
|
|
54
78
|
root?: string | undefined;
|
|
@@ -58,6 +82,7 @@ export declare function neighbors({ root, scope, nodeId, depth, limit }?: {
|
|
|
58
82
|
limit?: number | undefined;
|
|
59
83
|
}): Array<{
|
|
60
84
|
scope: StorageScope;
|
|
85
|
+
freshness: FreshnessReport;
|
|
61
86
|
} & NeighborRow>;
|
|
62
87
|
export declare function callGraph({ root, scope, term, kind, depth, limit }?: {
|
|
63
88
|
root?: string | undefined;
|
|
@@ -68,6 +93,7 @@ export declare function callGraph({ root, scope, term, kind, depth, limit }?: {
|
|
|
68
93
|
limit?: number | undefined;
|
|
69
94
|
}): Array<{
|
|
70
95
|
scope: StorageScope;
|
|
96
|
+
freshness: FreshnessReport;
|
|
71
97
|
} & CallGraphMatch>;
|
|
72
98
|
export interface ContextGraphOptions {
|
|
73
99
|
root?: string | undefined;
|
|
@@ -92,5 +118,6 @@ export declare function runReadOnlySql({ root, scope, sql, attachHome, limit }?:
|
|
|
92
118
|
}): Array<{
|
|
93
119
|
scope: StorageScope;
|
|
94
120
|
rows: unknown[];
|
|
121
|
+
freshness: FreshnessReport;
|
|
95
122
|
}>;
|
|
96
123
|
//# sourceMappingURL=repository.d.ts.map
|