@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
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
- The command compares a cheap path/size/mtime fingerprint with indexed files and
123
- reports whether the graph was reused or refreshed. This metadata check is not a
124
- content-verification guarantee for same-size edits with restored mtimes.
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. Use `lscg scan` when you want the full one-shot
136
- scan output.
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 starter scanner supports JavaScript, JSX, TypeScript, and TSX via
207
- Tree-sitter. The extraction layer records syntax-grounded edges such as
208
- `defines`, `contains`, `imports`, `exports`, and `calls`. Cross-file symbol
209
- resolution should be added as language-specific semantic enrichers.
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 { GraphNode, FileAttribution } from '../types.js';
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
- }): FileAttribution | null;
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 blameLines = blameFile(root, relativePath);
4
- if (!blameLines)
5
- return null;
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
- contributorEmails: [...contributorEmails.values()].sort((left, right) => left.email.localeCompare(right.email)),
30
- nodeAttributions
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
- const result = spawnSync('git', ['-C', root, 'blame', '--follow', '--line-porcelain', '--', relativePath], {
35
- encoding: 'utf8'
36
- });
37
- if (result.error || result.status !== 0 || !result.stdout.trim()) {
38
- return null;
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
- const lines = result.stdout.split(/\r?\n/);
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 ? blameLines : null;
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 declare function scanRepository({ root, scope, plugins, pluginLimits }?: {
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
- }): Promise<ScanSummary>;
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