@psnext/lscg 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -15
- package/dist/bin/lscg.js +0 -0
- package/dist/src/cli-progress.d.ts +7 -0
- package/dist/src/cli-progress.js +59 -0
- package/dist/src/cli.js +62 -9
- package/dist/src/explore/sigma-provider.d.ts +27 -0
- package/dist/src/explore/sigma-provider.js +87 -0
- package/dist/src/explore/sigma-render.d.ts +18 -0
- package/dist/src/explore/sigma-render.js +67 -0
- package/dist/src/graph/attribution.d.ts +2 -2
- package/dist/src/graph/attribution.js +36 -13
- package/dist/src/graph/explore.d.ts +20 -0
- package/dist/src/graph/explore.js +200 -0
- package/dist/src/graph/repository.d.ts +36 -4
- package/dist/src/graph/repository.js +443 -159
- package/dist/src/graph/repositoryScanWorker.d.ts +21 -0
- package/dist/src/graph/repositoryScanWorker.js +45 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +5 -0
- package/dist/src/mcp/server.js +21 -3
- 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/attributionPlugin.d.ts +5 -0
- package/dist/src/scanner/attributionPlugin.js +16 -0
- package/dist/src/scanner/discover.js +89 -16
- 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 +38 -4
- package/dist/src/scanner/plugins.js +58 -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 +85 -0
- package/dist/src/storage/database.d.ts +1 -0
- package/dist/src/storage/database.js +1 -0
- package/dist/src/storage/explore-queries.d.ts +52 -0
- package/dist/src/storage/explore-queries.js +184 -0
- package/dist/src/storage/graph-writes.d.ts +22 -3
- package/dist/src/storage/graph-writes.js +167 -20
- package/dist/src/storage/manifest-inventory.d.ts +23 -0
- package/dist/src/storage/manifest-inventory.js +82 -0
- package/dist/src/storage/plugin-graph.js +3 -3
- package/dist/src/storage/queries.d.ts +10 -3
- package/dist/src/storage/queries.js +99 -24
- package/dist/src/storage/schema.d.ts +2 -2
- package/dist/src/storage/schema.js +48 -1
- package/dist/src/types.d.ts +110 -6
- package/dist/src/watch.d.ts +20 -2
- package/dist/src/watch.js +211 -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,14 +133,78 @@ 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
|
+
```
|
|
137
157
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
200
|
+
|
|
201
|
+
Git user attribution is optional. Enable it with `--attribution` on `scan`,
|
|
202
|
+
`watch`, or `scan --watch`; normal scans return structural data with attribution
|
|
203
|
+
pending, while `--full` and active watch sessions drain the selected enrichment.
|
|
204
|
+
Without the flag, scans report attribution as `disabled` and do not invoke Git
|
|
205
|
+
or create user nodes. Existing attribution remains readable if a later scan omits
|
|
206
|
+
the flag. When enabled, contributor user nodes are keyed by normalized email and
|
|
207
|
+
the most recent contributor is tracked for each discovered node.
|
|
141
208
|
|
|
142
209
|
## Scanner plugins
|
|
143
210
|
|
|
@@ -147,10 +214,11 @@ with the resolved `repoPath`, discovered files, file contents, and a `readFile`
|
|
|
147
214
|
helper, then returns semantic node and edge facts. Edges refer to node `factKey` values within their emitting plugin namespace and
|
|
148
215
|
may connect facts from different files. Use the additive `sourcePlugin` and
|
|
149
216
|
`targetPlugin` fields when an endpoint belongs to another plugin; raw fact keys
|
|
150
|
-
are never parsed as namespaces.
|
|
217
|
+
are never parsed as namespaces. Nodes and edges may also carry plugin-defined
|
|
218
|
+
`type` values; edge `type` defaults to its structural `kind` when omitted.
|
|
151
219
|
|
|
152
220
|
```ts
|
|
153
|
-
import { scanRepository, type ScannerPlugin } from '@psnext/lscg';
|
|
221
|
+
import { AttributionPlugin, scanRepository, type ScannerPlugin } from '@psnext/lscg';
|
|
154
222
|
|
|
155
223
|
const plugin: ScannerPlugin = {
|
|
156
224
|
name: 'my-analyzer',
|
|
@@ -172,8 +240,26 @@ const plugin: ScannerPlugin = {
|
|
|
172
240
|
};
|
|
173
241
|
|
|
174
242
|
await scanRepository({ root: '/path/to/repo', plugins: [plugin] });
|
|
243
|
+
|
|
244
|
+
// Or opt into the bundled Git user attribution enrichment:
|
|
245
|
+
await scanRepository({ root: '/path/to/repo', plugins: [AttributionPlugin] });
|
|
246
|
+
// The same bundled plugin can also be selected as a convenience option:
|
|
247
|
+
await scanRepository({ root: '/path/to/repo', attribution: true });
|
|
248
|
+
// The CLI equivalent is: lscg scan --attribution
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
A separately packaged plugin can be passed to a host application as an imported `ScannerPlugin`. The copyable [external scanner plugin example](examples/external-scanner-plugin/) demonstrates this with a TODO/FIXME analyzer and a host script.
|
|
252
|
+
|
|
253
|
+
The CLI can load the default export of an external module with the repeatable `--plugin` option:
|
|
254
|
+
|
|
255
|
+
```sh
|
|
256
|
+
lscg scan --root /path/to/repo --plugin @acme/my-scanner-plugin
|
|
257
|
+
lscg scan --root /path/to/repo --plugin ./my-scanner-plugin.mjs
|
|
258
|
+
lscg watch --root /path/to/repo --plugin ./my-scanner-plugin.mjs
|
|
175
259
|
```
|
|
176
260
|
|
|
261
|
+
Relative module specifiers are resolved from the current working directory. The module must default-export one `ScannerPlugin` (an array is also accepted). External plugins are trusted code and run in-process; only load modules you trust. The supplied plugins are used for one-shot scans, `watch`, and `scan --watch`.
|
|
262
|
+
|
|
177
263
|
Plugins may implement `initialize`, `scan`, `finalize`, and `dispose` hooks.
|
|
178
264
|
The core assigns deterministic IDs, validates API compatibility, persists
|
|
179
265
|
plugin facts, and reports plugin diagnostics in the scan summary. Plugins run
|
|
@@ -190,7 +276,8 @@ facts conflict by `(node, factKey)` and edge facts by `(edge, factKey)` across
|
|
|
190
276
|
plugins. `merge` retains both under distinct plugin-qualified identities,
|
|
191
277
|
`replace` uses priority (then plugin-name order for ties), and `reject` keeps
|
|
192
278
|
the incumbent and reports a diagnostic. The host assigns deterministic IDs;
|
|
193
|
-
plugins do not provide database IDs.
|
|
279
|
+
plugins do not provide database IDs. Use `lscg edges --type <edge-type>` to filter
|
|
280
|
+
persisted edge types.
|
|
194
281
|
|
|
195
282
|
Every scan also runs the bundled `PackagePlugin`. It reads the repository-root
|
|
196
283
|
`package.json` dependency sections (`dependencies`, `devDependencies`,
|
|
@@ -203,7 +290,22 @@ listed in `plugins`.
|
|
|
203
290
|
|
|
204
291
|
## Current parser support
|
|
205
292
|
|
|
206
|
-
The
|
|
207
|
-
Tree-sitter.
|
|
208
|
-
|
|
209
|
-
|
|
293
|
+
The scanner supports JavaScript, JSX, TypeScript, TSX, Python, and Java via
|
|
294
|
+
Tree-sitter. Python is handled by the bundled `PythonScannerPlugin`, and Java
|
|
295
|
+
by `JavaScannerPlugin` using the pinned native `tree-sitter-java@0.21.0`
|
|
296
|
+
grammar. Java records syntax-grounded classes, interfaces, enums, records,
|
|
297
|
+
annotation types, methods, constructors, fields, imports, method invocations,
|
|
298
|
+
containment, and explicit-public export approximations. Java calls/imports are
|
|
299
|
+
occurrences (no type, overload, constructor, or cross-file resolution), and
|
|
300
|
+
inherited/module APIs are not modeled. A Java parse error retains the prior
|
|
301
|
+
file contribution and marks freshness degraded until the file recovers.
|
|
302
|
+
|
|
303
|
+
Repositories with root or immediate-module `pom.xml`, `build.gradle`, or
|
|
304
|
+
`build.gradle.kts` files also receive `java-dependency` `package` nodes from
|
|
305
|
+
`JavaDependencyPlugin`. Manifest reading is bounded and non-executing; only
|
|
306
|
+
literal Maven coordinates/properties and literal Gradle dependency calls are
|
|
307
|
+
modeled. Dependencies remain independent from Java imports: no heuristic
|
|
308
|
+
coordinate matching or dependency-to-import edges are emitted. Native
|
|
309
|
+
Tree-sitter install scripts must remain allowlisted for clean installs.
|
|
310
|
+
Cross-file symbol resolution remains a language-specific semantic-enricher
|
|
311
|
+
concern.
|
package/dist/bin/lscg.js
CHANGED
|
File without changes
|
|
@@ -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,9 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
1
3
|
import { startMcpServer } from './mcp/server.js';
|
|
2
4
|
import { buildViewModel, loadViewSnapshot, renderSvgMarkup, viewGraph } from './view/index.js';
|
|
3
5
|
import { watchRepository } from './watch.js';
|
|
6
|
+
import { createProgressReporter } from './cli-progress.js';
|
|
4
7
|
import { graphStatus, initGraph, callGraph, contextGraph, listEdges, listNodes, listNodeText, neighbors, runReadOnlySql, scanRepository } from './graph/repository.js';
|
|
5
8
|
const operationalCommands = new Set([
|
|
6
9
|
'init', 'scan', 'watch', 'status', 'nodes', 'edges', 'neighbors',
|
|
@@ -13,20 +16,27 @@ export async function runCli(argv) {
|
|
|
13
16
|
return;
|
|
14
17
|
}
|
|
15
18
|
const options = parseOptions(rest);
|
|
19
|
+
const progress = options.progress === true ? createProgressReporter() : undefined;
|
|
16
20
|
switch (command) {
|
|
17
21
|
case 'init':
|
|
18
22
|
printJson(initGraph({ root: options.root, scope: options.scope ?? 'repo' }));
|
|
19
23
|
return;
|
|
20
|
-
case 'scan':
|
|
24
|
+
case 'scan': {
|
|
25
|
+
if (options.watch && options.full)
|
|
26
|
+
throw new Error('scan --full cannot be combined with --watch');
|
|
27
|
+
const plugins = await loadExternalPlugins(options.plugin ?? []);
|
|
21
28
|
if (options.watch) {
|
|
22
|
-
await watchRepository({ root: options.root, scope: options.scope ?? 'repo' }, { mode: 'scan --watch' });
|
|
29
|
+
await watchRepository({ root: options.root, scope: options.scope ?? 'repo', attribution: options.attribution === true, plugins }, { mode: 'scan --watch', progress });
|
|
23
30
|
return;
|
|
24
31
|
}
|
|
25
|
-
printJson(await scanRepository({ root: options.root, scope: options.scope ?? 'repo' }));
|
|
32
|
+
printJson(await scanRepository({ root: options.root, scope: options.scope ?? 'repo', plugins, attribution: options.attribution === true, full: options.full === true, progress }));
|
|
26
33
|
return;
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
}
|
|
35
|
+
case 'watch': {
|
|
36
|
+
const plugins = await loadExternalPlugins(options.plugin ?? []);
|
|
37
|
+
await watchRepository({ root: options.root, scope: options.scope ?? 'repo', attribution: options.attribution === true, plugins }, { mode: 'watch', progress });
|
|
29
38
|
return;
|
|
39
|
+
}
|
|
30
40
|
case 'status':
|
|
31
41
|
printJson(graphStatus({ root: options.root, scope: options.scope ?? 'repo' }));
|
|
32
42
|
return;
|
|
@@ -58,6 +68,7 @@ export async function runCli(argv) {
|
|
|
58
68
|
root: options.root,
|
|
59
69
|
scope: options.scope ?? 'repo',
|
|
60
70
|
kind: options.kind,
|
|
71
|
+
type: options.type,
|
|
61
72
|
limit: options.limit
|
|
62
73
|
});
|
|
63
74
|
const output = options.output ?? 'json';
|
|
@@ -233,12 +244,21 @@ function parseOptions(args) {
|
|
|
233
244
|
if (!rawKey)
|
|
234
245
|
continue;
|
|
235
246
|
const key = toCamelCase(rawKey);
|
|
247
|
+
if (key === 'plugin') {
|
|
248
|
+
const value = inlineValue ?? args[index + 1];
|
|
249
|
+
if (!value || value.startsWith('-'))
|
|
250
|
+
throw new Error('--plugin requires a module specifier');
|
|
251
|
+
options.plugin = [...(options.plugin ?? []), value];
|
|
252
|
+
if (inlineValue === undefined)
|
|
253
|
+
index += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
236
256
|
if (inlineValue !== undefined) {
|
|
237
257
|
options[key] = coerce(inlineValue);
|
|
238
258
|
continue;
|
|
239
259
|
}
|
|
240
260
|
const next = args[index + 1];
|
|
241
|
-
const booleanOption = key === 'watch' || key === 'attachHome' || key === 'excerpts' || key === 'help';
|
|
261
|
+
const booleanOption = key === 'watch' || key === 'full' || key === 'attachHome' || key === 'excerpts' || key === 'help' || key === 'progress' || key === 'attribution';
|
|
242
262
|
if (!booleanOption && next && !next.startsWith('--')) {
|
|
243
263
|
options[key] = coerce(next);
|
|
244
264
|
index += 1;
|
|
@@ -286,11 +306,11 @@ function validateContextOptions(options, args) {
|
|
|
286
306
|
function commandHelpText(command) {
|
|
287
307
|
const help = {
|
|
288
308
|
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`,
|
|
309
|
+
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 --attribution Enable optional Git user attribution enrichment.\n --plugin module Load an external ScannerPlugin module (repeatable).\n\nExamples:\n lscg scan --scope both\n lscg scan --full\n lscg scan --watch`,
|
|
310
|
+
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 --attribution Enable optional Git user attribution enrichment.\n --plugin module Load an external ScannerPlugin module (repeatable).\n\nExamples:\n lscg watch`,
|
|
291
311
|
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
312
|
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
|
-
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`,
|
|
313
|
+
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 --type edge-type Filter by edge type.\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`,
|
|
294
314
|
neighbors: `Usage: lscg neighbors <node-id> [options]\n\nShow nearby nodes around a graph node id.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo|home|both Storage scope to inspect (default: repo).\n --depth n Traversal depth (default: 1).\n --limit n Maximum number of neighbors (default: 100).\n --node-id id Node ID alternative to the positional argument.\n\nExamples:\n lscg neighbors <node-id> --depth 2`,
|
|
295
315
|
callgraph: `Usage: lscg callgraph <term> [options]\n\nFind nodes whose names contain a term and show upstream/downstream nodes.\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 matching nodes by kind.\n --depth n Upstream/downstream traversal depth (default: 5).\n --limit n Maximum matches and relations (default: 100).\n --output json|text|svg Output format (default: text; svg requires repo or home scope).\n -o value Alias for --output.\n\nExamples:\n lscg callgraph greet --depth 2\n lscg callgraph greet --output text`,
|
|
296
316
|
context: `Usage: lscg context <symbol> [options]\n\nRetrieve bounded definition, impact, and dependency context.\nOptions:\n --root path Repository root (default: current directory).\n --scope repo|home|both Storage scope to inspect (default: repo).\n --kind node-kind Constrain anchor candidates by node kind.\n --file relative/path Constrain anchor candidates by repository-relative path.\n --depth 0..5 Relationship depth (default: 2).\n --limit 1..500 Relationships per section (default: 50).\n --candidate-limit 1..100 Maximum ranked candidates (default: 20).\n --excerpt-lines 1..500 Opt-in excerpt line budget (default: 80).\n --excerpt-bytes 1..100000 Opt-in excerpt byte budget (default: 12000).\n --excerpts Include bounded source excerpts.\n --output json|text|svg Output format (default: text; svg requires repo or home scope).\n -o value Alias for --output.\n\nExamples:\n lscg context greet --kind symbol\n lscg context greet --output text`,
|
|
@@ -303,6 +323,30 @@ function commandHelpText(command) {
|
|
|
303
323
|
return helpText();
|
|
304
324
|
return `${commandHelp}\n\nShort aliases (where supported): -r --root, -s --scope, -k --kind, -d --depth, -l --limit, -f --file, -o --output.`;
|
|
305
325
|
}
|
|
326
|
+
async function loadExternalPlugins(specifiers) {
|
|
327
|
+
const plugins = [];
|
|
328
|
+
for (const specifier of specifiers) {
|
|
329
|
+
const importTarget = specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:')
|
|
330
|
+
? (specifier.startsWith('file:') ? specifier : pathToFileURL(path.resolve(process.cwd(), specifier)).href)
|
|
331
|
+
: specifier;
|
|
332
|
+
let loaded;
|
|
333
|
+
try {
|
|
334
|
+
loaded = await import(importTarget);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
throw new Error(`failed to load external plugin ${specifier}: ${error instanceof Error ? error.message : String(error)}`);
|
|
338
|
+
}
|
|
339
|
+
const candidate = loaded.default ?? loaded;
|
|
340
|
+
const values = Array.isArray(candidate) ? candidate : [candidate];
|
|
341
|
+
for (const plugin of values) {
|
|
342
|
+
if (!plugin || typeof plugin !== 'object' || typeof plugin.name !== 'string' || typeof plugin.scan !== 'function') {
|
|
343
|
+
throw new Error(`external plugin ${specifier} must default-export a ScannerPlugin or ScannerPlugin[]`);
|
|
344
|
+
}
|
|
345
|
+
plugins.push(plugin);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return plugins;
|
|
349
|
+
}
|
|
306
350
|
function toCamelCase(value) {
|
|
307
351
|
return value.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
308
352
|
}
|
|
@@ -479,10 +523,17 @@ Command options:
|
|
|
479
523
|
scan:
|
|
480
524
|
--root path Repository root (default: current directory).
|
|
481
525
|
--scope repo|home|both Storage scope to scan (default: repo).
|
|
526
|
+
--full Rebuild and verify every discovered source file.
|
|
482
527
|
--watch Watch and rescan after the initial scan.
|
|
528
|
+
--progress Write human-readable lifecycle progress to stderr.
|
|
529
|
+
--attribution Enable optional Git user attribution enrichment.
|
|
530
|
+
--plugin module Load an external ScannerPlugin module (repeatable).
|
|
483
531
|
watch:
|
|
484
532
|
--root path Repository root (default: current directory).
|
|
485
533
|
--scope repo Repository scope to watch (default: repo).
|
|
534
|
+
--progress Write human-readable lifecycle progress to stderr.
|
|
535
|
+
--attribution Enable optional Git user attribution enrichment.
|
|
536
|
+
--plugin module Load an external ScannerPlugin module (repeatable).
|
|
486
537
|
status:
|
|
487
538
|
--root path Repository root (default: current directory).
|
|
488
539
|
--scope repo|home|both Storage scope to inspect (default: repo).
|
|
@@ -496,6 +547,7 @@ Command options:
|
|
|
496
547
|
--root path Repository root (default: current directory).
|
|
497
548
|
--scope repo|home|both Storage scope to inspect (default: repo).
|
|
498
549
|
--kind edge-kind Filter by edge kind.
|
|
550
|
+
--type edge-type Filter by edge type.
|
|
499
551
|
--limit n Maximum number of edges (default: 50).
|
|
500
552
|
--output json|text Output format (default: json).
|
|
501
553
|
neighbors <node-id>:
|
|
@@ -570,6 +622,7 @@ Notes:
|
|
|
570
622
|
- lscg view opens an interactive HTML page by default.
|
|
571
623
|
- Pass --output to write a static SVG snapshot instead of opening the browser.
|
|
572
624
|
- lscg watch supports repo scope only.
|
|
625
|
+
- --progress is opt-in human-readable diagnostics on stderr; stdout JSON is unchanged.
|
|
573
626
|
|
|
574
627
|
Storage:
|
|
575
628
|
repo: <root>/.sling/graph.sqlite
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ExploreGraphResult } from '../types.js';
|
|
2
|
+
export interface SigmaGraph {
|
|
3
|
+
order: number;
|
|
4
|
+
size: number;
|
|
5
|
+
hasNode(nodeId: string): boolean;
|
|
6
|
+
hasEdge(edgeId: string): boolean;
|
|
7
|
+
addNode(nodeId: string, attributes: Record<string, unknown>): void;
|
|
8
|
+
addDirectedEdgeWithKey(edgeId: string, sourceId: string, targetId: string, attributes: Record<string, unknown>): void;
|
|
9
|
+
source(edgeId: string): string;
|
|
10
|
+
target(edgeId: string): string;
|
|
11
|
+
}
|
|
12
|
+
export interface SigmaGraphStats {
|
|
13
|
+
nodes: number;
|
|
14
|
+
edges: number;
|
|
15
|
+
aggregateNodes: number;
|
|
16
|
+
sourceNodes: number;
|
|
17
|
+
relationshipKinds: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Convert the provider-neutral Explore result into Sigma's Graphology model.
|
|
21
|
+
* This adapter owns no layout or browser lifecycle; those remain provider work.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toSigmaGraph(result: ExploreGraphResult): {
|
|
24
|
+
graph: SigmaGraph;
|
|
25
|
+
stats: SigmaGraphStats;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=sigma-provider.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Graphology from 'graphology';
|
|
2
|
+
/**
|
|
3
|
+
* Convert the provider-neutral Explore result into Sigma's Graphology model.
|
|
4
|
+
* This adapter owns no layout or browser lifecycle; those remain provider work.
|
|
5
|
+
*/
|
|
6
|
+
export function toSigmaGraph(result) {
|
|
7
|
+
const GraphConstructor = Graphology;
|
|
8
|
+
const graph = new GraphConstructor({ type: 'directed', multi: true, allowSelfLoops: true });
|
|
9
|
+
const aggregateNodes = result.scopes.flatMap((scope) => scope.nodes.filter((node) => node.category === 'aggregate'));
|
|
10
|
+
const sourceNodes = result.scopes.flatMap((scope) => scope.nodes.filter((node) => node.category === 'source'));
|
|
11
|
+
const edges = result.scopes.flatMap((scope) => scope.edges);
|
|
12
|
+
const allNodes = [...aggregateNodes, ...sourceNodes];
|
|
13
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(allNodes.length)));
|
|
14
|
+
allNodes.forEach((node, index) => {
|
|
15
|
+
if (graph.hasNode(node.id))
|
|
16
|
+
return;
|
|
17
|
+
graph.addNode(node.id, {
|
|
18
|
+
...sigmaNodeAttributes(node),
|
|
19
|
+
x: (index % columns) * 2,
|
|
20
|
+
y: Math.floor(index / columns) * 2
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
for (const edge of edges) {
|
|
24
|
+
if (!graph.hasNode(edge.sourceId) || !graph.hasNode(edge.targetId))
|
|
25
|
+
continue;
|
|
26
|
+
if (graph.hasEdge(edge.id))
|
|
27
|
+
continue;
|
|
28
|
+
graph.addDirectedEdgeWithKey(edge.id, edge.sourceId, edge.targetId, {
|
|
29
|
+
label: edge.kind,
|
|
30
|
+
type: edge.kind,
|
|
31
|
+
size: edge.count ? Math.max(1, Math.log2(edge.count + 1)) : 1,
|
|
32
|
+
color: edgeColor(edge.kind),
|
|
33
|
+
confidence: edge.confidence,
|
|
34
|
+
metadata: edge.metadata
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
graph,
|
|
39
|
+
stats: {
|
|
40
|
+
nodes: graph.order,
|
|
41
|
+
edges: graph.size,
|
|
42
|
+
aggregateNodes: aggregateNodes.length,
|
|
43
|
+
sourceNodes: sourceNodes.length,
|
|
44
|
+
relationshipKinds: [...new Set(edges.map((edge) => edge.kind))].sort()
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function sigmaNodeAttributes(node) {
|
|
49
|
+
if (node.category === 'aggregate') {
|
|
50
|
+
return {
|
|
51
|
+
label: node.name,
|
|
52
|
+
x: 0,
|
|
53
|
+
y: 0,
|
|
54
|
+
size: Math.max(4, Math.min(18, 4 + Math.log2(node.childCount + 1))),
|
|
55
|
+
color: '#2563eb',
|
|
56
|
+
category: node.category,
|
|
57
|
+
kind: node.kind,
|
|
58
|
+
path: node.path,
|
|
59
|
+
expandable: node.expandable,
|
|
60
|
+
expansionKey: node.expansionKey
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
label: node.name ?? node.id,
|
|
65
|
+
x: 0,
|
|
66
|
+
y: 0,
|
|
67
|
+
size: 5,
|
|
68
|
+
color: '#64748b',
|
|
69
|
+
category: node.category,
|
|
70
|
+
kind: node.kind,
|
|
71
|
+
type: node.type,
|
|
72
|
+
path: node.path,
|
|
73
|
+
metadata: node.metadata,
|
|
74
|
+
expansionKey: node.expansionKey
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function edgeColor(kind) {
|
|
78
|
+
const colors = {
|
|
79
|
+
calls: '#dc2626',
|
|
80
|
+
imports: '#7c3aed',
|
|
81
|
+
exports: '#0891b2',
|
|
82
|
+
contains: '#64748b',
|
|
83
|
+
defines: '#16a34a'
|
|
84
|
+
};
|
|
85
|
+
return colors[kind] ?? '#94a3b8';
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=sigma-provider.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type BrowserOpenResult } from '../view/open.js';
|
|
2
|
+
import type { ExploreGraphResult } from '../types.js';
|
|
3
|
+
export interface SigmaExploreRunResult {
|
|
4
|
+
mode: 'interactive';
|
|
5
|
+
artifactPath: string;
|
|
6
|
+
opened: boolean;
|
|
7
|
+
command?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
nodeCount: number;
|
|
10
|
+
edgeCount: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SigmaExploreDependencies {
|
|
13
|
+
opener?: (targetPath: string) => BrowserOpenResult;
|
|
14
|
+
}
|
|
15
|
+
/** Render the provider-neutral result into a standalone Sigma.js HTML page. */
|
|
16
|
+
export declare function renderSigmaExploreHtml(result: ExploreGraphResult): string;
|
|
17
|
+
export declare function openSigmaExplore(result: ExploreGraphResult, dependencies?: SigmaExploreDependencies): SigmaExploreRunResult;
|
|
18
|
+
//# sourceMappingURL=sigma-render.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { openHtmlArtifactInBrowser } from '../view/open.js';
|
|
2
|
+
/** Render the provider-neutral result into a standalone Sigma.js HTML page. */
|
|
3
|
+
export function renderSigmaExploreHtml(result) {
|
|
4
|
+
const serialized = JSON.stringify(result).replaceAll('<', '\\u003c');
|
|
5
|
+
const title = `${result.repository.name} Explore`;
|
|
6
|
+
return `<!doctype html>
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<title>${escapeHtml(title)}</title>
|
|
12
|
+
<style>
|
|
13
|
+
:root { color-scheme: dark; font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
14
|
+
html, body, #app { width: 100%; height: 100%; margin: 0; background: #020617; color: #e2e8f0; }
|
|
15
|
+
#graph { position: absolute; inset: 0; }
|
|
16
|
+
#panel { position: absolute; top: 16px; left: 16px; width: min(360px, calc(100vw - 32px)); max-height: calc(100vh - 32px); overflow: auto; padding: 16px; border: 1px solid #334155; border-radius: 12px; background: rgba(15, 23, 42, .94); box-shadow: 0 12px 32px rgba(0,0,0,.35); }
|
|
17
|
+
h1 { margin: 0 0 6px; font-size: 18px; } h2 { margin: 16px 0 8px; font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: #94a3b8; }
|
|
18
|
+
#status { color: #94a3b8; font-size: 12px; white-space: pre-wrap; }
|
|
19
|
+
input { width: 100%; box-sizing: border-box; margin-top: 12px; padding: 8px 10px; border: 1px solid #475569; border-radius: 7px; background: #0f172a; color: inherit; }
|
|
20
|
+
label { display: block; margin: 6px 0; font-size: 12px; color: #cbd5e1; } label input { width: auto; margin: 0 6px 0 0; }
|
|
21
|
+
#details { font-size: 12px; line-height: 1.5; color: #cbd5e1; }
|
|
22
|
+
.warning { color: #fbbf24; } code { color: #93c5fd; }
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body><div id="app"><div id="graph"></div><aside id="panel">
|
|
26
|
+
<h1>${escapeHtml(title)}</h1><div id="status"></div>
|
|
27
|
+
<input id="search" type="search" placeholder="Search nodes">
|
|
28
|
+
<h2>Relationship filters</h2><div id="filters"></div>
|
|
29
|
+
<h2>Selected node</h2><div id="details">Click a node to inspect it.</div>
|
|
30
|
+
</aside></div>
|
|
31
|
+
<script type="module">
|
|
32
|
+
import Graph from 'https://cdn.jsdelivr.net/npm/graphology@0.26.0/+esm';
|
|
33
|
+
import Sigma from 'https://cdn.jsdelivr.net/npm/sigma@3.0.3/+esm';
|
|
34
|
+
const result = ${serialized};
|
|
35
|
+
const scope = result.scopes[0] || { nodes: [], edges: [], warnings: [], filters: { relationshipKinds: [] }, freshness: { state: 'missing' } };
|
|
36
|
+
const graph = new Graph({ type: 'directed', multi: true, allowSelfLoops: true });
|
|
37
|
+
const nodes = scope.nodes || [], edges = scope.edges || [];
|
|
38
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(nodes.length)));
|
|
39
|
+
for (const [index, node] of nodes.entries()) {
|
|
40
|
+
graph.addNode(node.id, { label: node.name || node.id, x: (index % columns) * 2, y: Math.floor(index / columns) * 2, size: node.category === 'aggregate' ? Math.max(5, Math.min(18, 5 + Math.log2((node.childCount || 0) + 1))) : 5, color: node.category === 'aggregate' ? '#2563eb' : '#64748b', kind: node.kind, category: node.category, hidden: false });
|
|
41
|
+
}
|
|
42
|
+
for (const edge of edges) {
|
|
43
|
+
if (graph.hasNode(edge.sourceId) && graph.hasNode(edge.targetId) && !graph.hasEdge(edge.id)) graph.addDirectedEdgeWithKey(edge.id, edge.sourceId, edge.targetId, { type: 'arrow', label: edge.kind, color: edgeColor(edge.kind), size: edge.count ? Math.max(1, Math.log2(edge.count + 1)) : 1, relationshipKind: edge.kind });
|
|
44
|
+
}
|
|
45
|
+
const visibleKinds = new Set(scope.filters?.relationshipKinds || []);
|
|
46
|
+
const search = document.getElementById('search');
|
|
47
|
+
const details = document.getElementById('details');
|
|
48
|
+
const status = document.getElementById('status');
|
|
49
|
+
const filters = document.getElementById('filters');
|
|
50
|
+
status.textContent = 'Mode: ' + scope.mode + '\\nScope: ' + scope.scope + '\\nNodes: ' + graph.order + ' · Edges: ' + graph.size + '\\nFreshness: ' + scope.freshness.state + (scope.truncation?.bounded ? '\\nResult bounded by visibility caps.' : '');
|
|
51
|
+
if (scope.warnings?.length) status.textContent += '\\n' + scope.warnings.join('\\n');
|
|
52
|
+
for (const kind of ['contains', 'defines', 'calls', 'imports', 'exports']) { const label = document.createElement('label'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = visibleKinds.has(kind); checkbox.addEventListener('change', () => { checkbox.checked ? visibleKinds.add(kind) : visibleKinds.delete(kind); renderer.refresh(); }); label.append(checkbox, kind); filters.append(label); }
|
|
53
|
+
const renderer = new Sigma(graph, document.getElementById('graph'), { renderEdgeLabels: false, labelColor: { color: '#e2e8f0' }, nodeReducer: (node, attrs) => { const next = { ...attrs }; const term = search.value.trim().toLocaleLowerCase(); if (term && !String(attrs.label || '').toLocaleLowerCase().includes(term)) next.hidden = true; return next; }, edgeReducer: (edge, attrs) => ({ ...attrs, hidden: !visibleKinds.has(String(attrs.relationshipKind)) }) });
|
|
54
|
+
search.addEventListener('input', () => renderer.refresh());
|
|
55
|
+
renderer.on('clickNode', ({ node }) => { const attrs = graph.getNodeAttributes(node); details.innerHTML = '<code>' + escapeHtml(node) + '</code><br>kind: ' + escapeHtml(String(attrs.kind || '')) + '<br>label: ' + escapeHtml(String(attrs.label || '')); });
|
|
56
|
+
function edgeColor(kind) { return ({ calls: '#dc2626', imports: '#7c3aed', exports: '#0891b2', contains: '#64748b', defines: '#16a34a' })[kind] || '#94a3b8'; }
|
|
57
|
+
function escapeHtml(value) { return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); }
|
|
58
|
+
</script></body></html>`;
|
|
59
|
+
}
|
|
60
|
+
export function openSigmaExplore(result, dependencies = {}) {
|
|
61
|
+
const opened = openHtmlArtifactInBrowser(renderSigmaExploreHtml(result), dependencies.opener);
|
|
62
|
+
const scope = result.scopes.reduce((total, item) => total + item.nodes.length, 0);
|
|
63
|
+
const edges = result.scopes.reduce((total, item) => total + item.edges.length, 0);
|
|
64
|
+
return { mode: 'interactive', artifactPath: opened.htmlPath, opened: opened.opened, ...(opened.command ? { command: opened.command } : {}), ...(opened.error ? { error: opened.error } : {}), nodeCount: scope, edgeCount: edges };
|
|
65
|
+
}
|
|
66
|
+
function escapeHtml(value) { return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); }
|
|
67
|
+
//# sourceMappingURL=sigma-render.js.map
|
|
@@ -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
|