@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
|
@@ -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);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ExploreDirection, ExploreEdgeKind, ExploreGraphResult, GraphScope } from '../types.js';
|
|
2
|
+
export declare const EXPLORE_DEFAULT_DEPTH = 4;
|
|
3
|
+
export declare const EXPLORE_MAX_NODES = 2000;
|
|
4
|
+
export declare const EXPLORE_MAX_EDGES = 10000;
|
|
5
|
+
export declare const EXPLORE_EDGE_KINDS: readonly ExploreEdgeKind[];
|
|
6
|
+
export interface ExploreGraphOptions {
|
|
7
|
+
root?: string | undefined;
|
|
8
|
+
scope?: GraphScope;
|
|
9
|
+
anchor?: string | undefined;
|
|
10
|
+
file?: string | undefined;
|
|
11
|
+
search?: string | undefined;
|
|
12
|
+
direction?: ExploreDirection | undefined;
|
|
13
|
+
relationshipKinds?: readonly ExploreEdgeKind[] | undefined;
|
|
14
|
+
depth?: number | undefined;
|
|
15
|
+
nodeCap?: number | undefined;
|
|
16
|
+
edgeCap?: number | undefined;
|
|
17
|
+
candidateLimit?: number | undefined;
|
|
18
|
+
}
|
|
19
|
+
export declare function exploreGraph(options?: ExploreGraphOptions): ExploreGraphResult;
|
|
20
|
+
//# sourceMappingURL=explore.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { databasePathForScope, normalizeScope } from '../config/paths.js';
|
|
3
|
+
import { aggregateId, closeDatabase, openReadOnlyGraphDatabase, selectExploreAggregateEdges, selectExploreAggregates, selectExploreCandidates, selectExploreNode, selectExploreNodeRows, selectExploreSourcesForSearch, selectExploreTraversalEdges, selectFreshnessReport } from '../storage/database.js';
|
|
4
|
+
import { repositoryForRoot } from './repository.js';
|
|
5
|
+
export const EXPLORE_DEFAULT_DEPTH = 4;
|
|
6
|
+
export const EXPLORE_MAX_NODES = 2_000;
|
|
7
|
+
export const EXPLORE_MAX_EDGES = 10_000;
|
|
8
|
+
export const EXPLORE_EDGE_KINDS = ['contains', 'defines', 'calls', 'imports', 'exports'];
|
|
9
|
+
export function exploreGraph(options = {}) {
|
|
10
|
+
const repository = repositoryForRoot(options.root);
|
|
11
|
+
const scope = options.scope ?? 'repo';
|
|
12
|
+
const mode = options.anchor || options.file ? 'anchor' : 'full';
|
|
13
|
+
const request = {
|
|
14
|
+
root: repository.root,
|
|
15
|
+
scope,
|
|
16
|
+
mode,
|
|
17
|
+
...(options.anchor ? { anchor: options.anchor.trim() } : {}),
|
|
18
|
+
...(options.file ? { file: options.file.trim() } : {}),
|
|
19
|
+
...(options.search ? { search: options.search.trim() } : {}),
|
|
20
|
+
direction: options.direction ?? 'both',
|
|
21
|
+
relationshipKinds: normalizeKinds(options.relationshipKinds),
|
|
22
|
+
depth: clamp(options.depth, 0, EXPLORE_DEFAULT_DEPTH, EXPLORE_DEFAULT_DEPTH),
|
|
23
|
+
nodeCap: clamp(options.nodeCap, 1, EXPLORE_MAX_NODES, EXPLORE_MAX_NODES),
|
|
24
|
+
edgeCap: clamp(options.edgeCap, 1, EXPLORE_MAX_EDGES, EXPLORE_MAX_EDGES),
|
|
25
|
+
candidateLimit: clamp(options.candidateLimit, 1, 100, 20)
|
|
26
|
+
};
|
|
27
|
+
const scopes = normalizeScope(scope);
|
|
28
|
+
return {
|
|
29
|
+
contract_version: 1,
|
|
30
|
+
repository,
|
|
31
|
+
request,
|
|
32
|
+
scopes: scopes.map((graphScope) => exploreScope(repository, graphScope, request))
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function exploreScope(repository, graphScope, request) {
|
|
36
|
+
const databasePath = databasePathForScope(graphScope, repository.root);
|
|
37
|
+
if (!existsSync(databasePath))
|
|
38
|
+
return emptyScope(graphScope, databasePath, request, missingFreshness(databasePath));
|
|
39
|
+
let db;
|
|
40
|
+
try {
|
|
41
|
+
db = openReadOnlyGraphDatabase(databasePath);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return emptyScope(graphScope, databasePath, request, {
|
|
45
|
+
state: 'unavailable', reason: 'unavailable_storage', message: `Explore storage is unavailable: ${diagnostic(error)}`,
|
|
46
|
+
recovery: 'Run lscg scan explicitly, then retry Explore.'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const persistedFreshness = selectFreshnessReport(db, repository.id);
|
|
51
|
+
const freshness = freshnessFor(persistedFreshness);
|
|
52
|
+
if (request.mode === 'full')
|
|
53
|
+
return fullScope(db, repository, graphScope, databasePath, request, freshness);
|
|
54
|
+
return anchorScope(db, repository, graphScope, databasePath, request, freshness);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
closeDatabase(db);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function fullScope(db, repository, scope, databasePath, request, freshness) {
|
|
61
|
+
const allAggregates = selectExploreAggregates(db, repository.id);
|
|
62
|
+
let searchState = { term: request.search ?? null, matchedNodeIds: [], containingAggregateIds: [] };
|
|
63
|
+
let aggregates = allAggregates;
|
|
64
|
+
let matchingSources = [];
|
|
65
|
+
if (request.search?.trim()) {
|
|
66
|
+
const normalized = request.search.trim().toLocaleLowerCase();
|
|
67
|
+
matchingSources = selectExploreSourcesForSearch(db, repository.id, request.search, 200);
|
|
68
|
+
const matchedNodeIds = matchingSources.map((row) => row.id);
|
|
69
|
+
const containingAggregateIds = [...new Set(matchingSources.flatMap((row) => row.fileId ? [aggregateId(repository.id, row.fileId)] : []))];
|
|
70
|
+
const matchingFiles = new Set(containingAggregateIds);
|
|
71
|
+
aggregates = allAggregates.filter((row) => matchingFiles.has(row.id) || row.path.toLocaleLowerCase().includes(normalized));
|
|
72
|
+
searchState = { term: request.search.trim(), matchedNodeIds, containingAggregateIds };
|
|
73
|
+
}
|
|
74
|
+
const aggregateNodes = aggregates.map((row) => aggregateNode(row, repository.id, scope));
|
|
75
|
+
const nodes = [...aggregateNodes, ...matchingSources.map((row) => sourceNode(row, repository.id, scope))];
|
|
76
|
+
const aggregateIds = new Set(aggregateNodes.map((node) => node.id));
|
|
77
|
+
const edges = selectExploreAggregateEdges(db, repository.id, request.relationshipKinds)
|
|
78
|
+
.filter((edge) => aggregateIds.has(edge.sourceAggregateId) && aggregateIds.has(edge.targetAggregateId))
|
|
79
|
+
.map((edge) => ({ id: edge.id, sourceId: edge.sourceAggregateId, targetId: edge.targetAggregateId, kind: edge.kind, confidence: edge.confidence, direction: 'forward', metadata: edge.metadata, count: edge.count, expansionKey: `aggregate-expand:${edge.sourceAggregateId}:${edge.targetAggregateId}:${edge.kind}` }));
|
|
80
|
+
const state = nodes.length > 0 ? 'ok' : 'empty';
|
|
81
|
+
return {
|
|
82
|
+
scope, databasePath, freshness, state, mode: 'full', nodes, edges,
|
|
83
|
+
search: searchState, filters: { direction: request.direction, relationshipKinds: [...request.relationshipKinds] },
|
|
84
|
+
truncation: noTruncation(request.nodeCap, request.edgeCap), expansionTargets: nodes.filter((node) => node.category === 'aggregate' && node.expandable).map((node) => node.expansionKey), warnings: freshnessWarnings(freshness)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function anchorScope(db, repository, scope, databasePath, request, freshness) {
|
|
88
|
+
const term = request.file ?? request.anchor ?? '';
|
|
89
|
+
const candidates = selectExploreCandidates(db, repository.id, term, { fileAnchor: Boolean(request.file), limit: request.candidateLimit });
|
|
90
|
+
const candidateContract = candidates.map(candidateContractFromRow);
|
|
91
|
+
const base = {
|
|
92
|
+
scope, databasePath, freshness, mode: 'anchor',
|
|
93
|
+
search: { term: null, matchedNodeIds: [], containingAggregateIds: [] },
|
|
94
|
+
filters: { direction: request.direction, relationshipKinds: [...request.relationshipKinds] },
|
|
95
|
+
expansionTargets: [], warnings: freshnessWarnings(freshness)
|
|
96
|
+
};
|
|
97
|
+
if (candidates.length === 0)
|
|
98
|
+
return { ...base, state: 'not_found', nodes: [], edges: [], candidates: [], truncation: noTruncation(request.nodeCap, request.edgeCap) };
|
|
99
|
+
if (candidates.length !== 1)
|
|
100
|
+
return { ...base, state: 'ambiguous', nodes: [], edges: [], candidates: candidateContract, truncation: noTruncation(request.nodeCap, request.edgeCap), warnings: [...base.warnings, 'Select one ranked candidate before traversal.'] };
|
|
101
|
+
const selected = candidates[0];
|
|
102
|
+
if (selected.kind === 'file' && selected.aggregateId) {
|
|
103
|
+
const aggregate = selectExploreAggregates(db, repository.id).find((row) => row.id === selected.aggregateId);
|
|
104
|
+
const node = aggregate ? aggregateNode(aggregate, repository.id, scope) : null;
|
|
105
|
+
return { ...base, state: node ? 'ok' : 'not_found', nodes: node ? [node] : [], edges: [], ...(node ? { anchor: node } : {}), candidates: candidateContract, expansionTargets: node ? [node.expansionKey] : [], truncation: noTruncation(request.nodeCap, request.edgeCap) };
|
|
106
|
+
}
|
|
107
|
+
const anchor = selectExploreNode(db, repository.id, selected.id);
|
|
108
|
+
if (!anchor)
|
|
109
|
+
return { ...base, state: 'not_found', nodes: [], edges: [], candidates: candidateContract, truncation: noTruncation(request.nodeCap, request.edgeCap) };
|
|
110
|
+
const traversal = traverse(db, repository.id, anchor, request);
|
|
111
|
+
const nodes = traversal.nodes.map((row) => sourceNode(row, repository.id, scope));
|
|
112
|
+
const anchorNode = nodes.find((node) => node.category === 'source' && node.id === anchor.id) ?? sourceNode(anchor, repository.id, scope);
|
|
113
|
+
return {
|
|
114
|
+
...base, state: 'ok', nodes, edges: traversal.edges.map((edge) => edgeContract(edge, traversal.nodeIds)), anchor: anchorNode, candidates: candidateContract,
|
|
115
|
+
truncation: traversal.truncation, expansionTargets: [anchorNode.expansionKey ?? `node-expand:${anchorNode.id}`]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function traverse(db, repositoryId, anchor, request) {
|
|
119
|
+
const nodeIds = new Set([anchor.id]);
|
|
120
|
+
const allEdges = new Map();
|
|
121
|
+
let frontier = [anchor.id];
|
|
122
|
+
let omittedNodes = 0;
|
|
123
|
+
let omittedEdges = 0;
|
|
124
|
+
let deepestDepth = 0;
|
|
125
|
+
for (let depth = 1; depth <= request.depth && frontier.length > 0; depth += 1) {
|
|
126
|
+
const rows = selectExploreTraversalEdges(db, repositoryId, frontier, request.direction, request.relationshipKinds, request.edgeCap - allEdges.size + 1);
|
|
127
|
+
if (rows.length > request.edgeCap - allEdges.size)
|
|
128
|
+
omittedEdges += rows.length - Math.max(0, request.edgeCap - allEdges.size);
|
|
129
|
+
const uniqueRows = rows.filter((row) => !allEdges.has(row.id));
|
|
130
|
+
const adjacentIds = [...new Set(uniqueRows.flatMap((row) => adjacentFor(row, frontier, request.direction)))].filter((id) => !nodeIds.has(id));
|
|
131
|
+
const adjacentRows = selectExploreNodeRowsBounded(db, repositoryId, adjacentIds);
|
|
132
|
+
adjacentRows.sort(nodeOrder);
|
|
133
|
+
const available = Math.max(0, request.nodeCap - nodeIds.size);
|
|
134
|
+
const retained = adjacentRows.slice(0, available);
|
|
135
|
+
omittedNodes += Math.max(0, adjacentRows.length - retained.length);
|
|
136
|
+
for (const row of retained)
|
|
137
|
+
nodeIds.add(row.id);
|
|
138
|
+
const retainedIds = new Set(retained.map((row) => row.id));
|
|
139
|
+
for (const row of uniqueRows) {
|
|
140
|
+
if (allEdges.size >= request.edgeCap) {
|
|
141
|
+
omittedEdges += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (nodeIds.has(row.sourceId) && nodeIds.has(row.targetId))
|
|
145
|
+
allEdges.set(row.id, row);
|
|
146
|
+
else if (retainedIds.has(row.sourceId) || retainedIds.has(row.targetId))
|
|
147
|
+
omittedEdges += 1;
|
|
148
|
+
}
|
|
149
|
+
const next = retained.map((row) => row.id);
|
|
150
|
+
if (next.length === 0)
|
|
151
|
+
break;
|
|
152
|
+
frontier = next;
|
|
153
|
+
deepestDepth = depth;
|
|
154
|
+
}
|
|
155
|
+
const rows = selectExploreNodeRowsBounded(db, repositoryId, [...nodeIds]);
|
|
156
|
+
rows.sort(nodeOrder);
|
|
157
|
+
return { nodes: rows, nodeIds, edges: [...allEdges.values()], truncation: { bounded: omittedNodes > 0 || omittedEdges > 0, nodeCap: request.nodeCap, edgeCap: request.edgeCap, omittedNodes, omittedEdges, deepestDepth } };
|
|
158
|
+
}
|
|
159
|
+
function adjacentFor(row, frontier, direction) {
|
|
160
|
+
const hasSource = frontier.includes(row.sourceId);
|
|
161
|
+
const hasTarget = frontier.includes(row.targetId);
|
|
162
|
+
if (direction === 'incoming')
|
|
163
|
+
return hasTarget ? [row.sourceId] : [];
|
|
164
|
+
if (direction === 'outgoing')
|
|
165
|
+
return hasSource ? [row.targetId] : [];
|
|
166
|
+
return [...(hasSource ? [row.targetId] : []), ...(hasTarget ? [row.sourceId] : [])];
|
|
167
|
+
}
|
|
168
|
+
function edgeContract(edge, nodeIds) {
|
|
169
|
+
return { id: edge.id, sourceId: edge.sourceId, targetId: edge.targetId, kind: edge.kind, confidence: edge.confidence, direction: 'forward', metadata: edge.metadata };
|
|
170
|
+
}
|
|
171
|
+
function selectExploreNodeRowsBounded(db, repositoryId, ids) {
|
|
172
|
+
const rows = [];
|
|
173
|
+
for (let index = 0; index < ids.length; index += 500)
|
|
174
|
+
rows.push(...selectExploreNodeRows(db, repositoryId, ids.slice(index, index + 500)));
|
|
175
|
+
return rows;
|
|
176
|
+
}
|
|
177
|
+
function aggregateNode(row, repositoryId, scope) {
|
|
178
|
+
return { id: row.id, category: 'aggregate', kind: 'file', name: row.path, identity: { repositoryId, scope, path: row.path, qualifiedName: row.path }, path: row.path, language: row.language, expandable: row.childCount > 0, expansionKey: `aggregate-expand:${row.id}`, childCount: row.childCount };
|
|
179
|
+
}
|
|
180
|
+
function sourceNode(row, repositoryId, scope) {
|
|
181
|
+
return { id: row.id, category: 'source', kind: row.kind, type: row.type, name: row.name, identity: { repositoryId, scope, path: row.path, qualifiedName: row.name && row.path ? `${row.path}:${row.name}` : row.name }, path: row.path, span: { start: row.startPoint, end: row.endPoint }, metadata: row.metadata, ...(row.fileId ? { expansionKey: `aggregate-expand:${aggregateId(repositoryId, row.fileId)}` } : {}) };
|
|
182
|
+
}
|
|
183
|
+
function candidateContractFromRow(row) {
|
|
184
|
+
return { id: row.id, rank: row.rank, match: row.match, category: row.aggregateId && row.id === row.aggregateId ? 'aggregate' : 'source', kind: row.kind, name: row.name, qualifiedName: row.qualifiedName, path: row.path, ...(row.kind === 'file' ? {} : { span: { start: row.startPoint, end: row.endPoint } }) };
|
|
185
|
+
}
|
|
186
|
+
function freshnessFor(report) {
|
|
187
|
+
const message = report.state === 'fresh' ? 'Stored graph is fresh.' : report.state === 'degraded' ? 'Stored graph is degraded; some scan facts were retained.' : 'Stored graph is stale; Explore does not scan implicitly.';
|
|
188
|
+
return report.state === 'fresh'
|
|
189
|
+
? { state: report.state, ...(report.reason ? { reason: report.reason } : {}), message }
|
|
190
|
+
: { state: report.state, ...(report.reason ? { reason: report.reason } : {}), message, recovery: 'Run lscg scan explicitly to refresh the stored graph.' };
|
|
191
|
+
}
|
|
192
|
+
function missingFreshness(databasePath) { return { state: 'missing', reason: 'missing_database', message: `No stored graph was found at ${databasePath}.`, recovery: 'Run lscg scan explicitly, then retry Explore.' }; }
|
|
193
|
+
function freshnessWarnings(freshness) { return freshness.state === 'fresh' ? [] : [freshness.message, ...(freshness.recovery ? [freshness.recovery] : [])]; }
|
|
194
|
+
function noTruncation(nodeCap, edgeCap) { return { bounded: false, nodeCap, edgeCap, omittedNodes: 0, omittedEdges: 0, deepestDepth: 0 }; }
|
|
195
|
+
function emptyScope(scope, databasePath, request, freshness) { return { scope, databasePath, freshness, state: 'empty', mode: request.mode, nodes: [], edges: [], search: { term: request.search ?? null, matchedNodeIds: [], containingAggregateIds: [] }, filters: { direction: request.direction, relationshipKinds: [...request.relationshipKinds] }, truncation: noTruncation(request.nodeCap, request.edgeCap), expansionTargets: [], warnings: freshnessWarnings(freshness) }; }
|
|
196
|
+
function normalizeKinds(kinds) { return [...new Set((kinds ?? EXPLORE_EDGE_KINDS).filter((kind) => EXPLORE_EDGE_KINDS.includes(kind)))]; }
|
|
197
|
+
function clamp(value, min, max, fallback) { return value === undefined || !Number.isFinite(value) ? fallback : Math.max(min, Math.min(max, Math.trunc(value))); }
|
|
198
|
+
function nodeOrder(a, b) { return `${a.path ?? ''}:${a.name ?? ''}:${a.id}`.localeCompare(`${b.path ?? ''}:${b.name ?? ''}:${b.id}`); }
|
|
199
|
+
function diagnostic(error) { return error instanceof Error ? error.message : String(error); }
|
|
200
|
+
//# sourceMappingURL=explore.js.map
|
|
@@ -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,37 @@ 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
|
+
/** Enables the bundled Git attribution enrichment plugin. */
|
|
19
|
+
attribution?: boolean | undefined;
|
|
18
20
|
pluginLimits?: Partial<PluginResourceLimits> | undefined;
|
|
19
|
-
|
|
21
|
+
full?: boolean | undefined;
|
|
22
|
+
enrichmentRunner?: EnrichmentRunner | undefined;
|
|
23
|
+
progress?: ProgressReporter | undefined;
|
|
24
|
+
/** Internal attempt label used by the Full Scan stabilization loop. */
|
|
25
|
+
attempt?: number | undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A full scan must not report success with hash-incompatible enrichment work.
|
|
29
|
+
* Rebuild and drain again when a source changes while attribution is running.
|
|
30
|
+
*/
|
|
31
|
+
export declare function scanRepository(options?: ScanRepositoryOptions): Promise<ScanSummary>;
|
|
32
|
+
export declare const defaultEnrichmentRunner: EnrichmentRunner;
|
|
33
|
+
export interface DrainRepositoryEnrichmentOptions {
|
|
34
|
+
root?: string | undefined;
|
|
35
|
+
scope?: StorageScope | 'both' | undefined;
|
|
36
|
+
enrichmentRunner?: EnrichmentRunner | undefined;
|
|
37
|
+
pluginName?: string | undefined;
|
|
38
|
+
historyFingerprint?: string | null | undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Drains persisted, hash-compatible attribution work. This is deliberately
|
|
42
|
+
* explicit: one-shot structural scans persist work but never start it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function drainRepositoryEnrichment({ root, scope, enrichmentRunner, pluginName, historyFingerprint }?: DrainRepositoryEnrichmentOptions): Promise<EnrichmentSummary>;
|
|
20
45
|
export declare function graphStatus({ root, scope }?: {
|
|
21
46
|
root?: string | undefined;
|
|
22
47
|
scope?: StorageScope | 'both' | undefined;
|
|
@@ -31,6 +56,7 @@ export declare function listNodes({ root, scope, kind, limit }?: {
|
|
|
31
56
|
limit?: number | undefined;
|
|
32
57
|
}): Array<{
|
|
33
58
|
scope: StorageScope;
|
|
59
|
+
freshness: FreshnessReport;
|
|
34
60
|
} & GraphNodeRow>;
|
|
35
61
|
export declare function listNodeText({ root, scope, kind, term, limit }?: {
|
|
36
62
|
root?: string | undefined;
|
|
@@ -41,14 +67,17 @@ export declare function listNodeText({ root, scope, kind, term, limit }?: {
|
|
|
41
67
|
}): Array<{
|
|
42
68
|
scope: StorageScope;
|
|
43
69
|
root: string;
|
|
70
|
+
freshness: FreshnessReport;
|
|
44
71
|
} & GraphNodeTextRow>;
|
|
45
|
-
export declare function listEdges({ root, scope, kind, limit }?: {
|
|
72
|
+
export declare function listEdges({ root, scope, kind, type, limit }?: {
|
|
46
73
|
root?: string | undefined;
|
|
47
74
|
scope?: StorageScope | 'both' | undefined;
|
|
48
75
|
kind?: string | undefined;
|
|
76
|
+
type?: string | undefined;
|
|
49
77
|
limit?: number | undefined;
|
|
50
78
|
}): Array<{
|
|
51
79
|
scope: StorageScope;
|
|
80
|
+
freshness: FreshnessReport;
|
|
52
81
|
} & GraphEdgeRow>;
|
|
53
82
|
export declare function neighbors({ root, scope, nodeId, depth, limit }?: {
|
|
54
83
|
root?: string | undefined;
|
|
@@ -58,6 +87,7 @@ export declare function neighbors({ root, scope, nodeId, depth, limit }?: {
|
|
|
58
87
|
limit?: number | undefined;
|
|
59
88
|
}): Array<{
|
|
60
89
|
scope: StorageScope;
|
|
90
|
+
freshness: FreshnessReport;
|
|
61
91
|
} & NeighborRow>;
|
|
62
92
|
export declare function callGraph({ root, scope, term, kind, depth, limit }?: {
|
|
63
93
|
root?: string | undefined;
|
|
@@ -68,6 +98,7 @@ export declare function callGraph({ root, scope, term, kind, depth, limit }?: {
|
|
|
68
98
|
limit?: number | undefined;
|
|
69
99
|
}): Array<{
|
|
70
100
|
scope: StorageScope;
|
|
101
|
+
freshness: FreshnessReport;
|
|
71
102
|
} & CallGraphMatch>;
|
|
72
103
|
export interface ContextGraphOptions {
|
|
73
104
|
root?: string | undefined;
|
|
@@ -92,5 +123,6 @@ export declare function runReadOnlySql({ root, scope, sql, attachHome, limit }?:
|
|
|
92
123
|
}): Array<{
|
|
93
124
|
scope: StorageScope;
|
|
94
125
|
rows: unknown[];
|
|
126
|
+
freshness: FreshnessReport;
|
|
95
127
|
}>;
|
|
96
128
|
//# sourceMappingURL=repository.d.ts.map
|