@psnext/lscg 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -9
- package/dist/src/cli-progress.d.ts +7 -0
- package/dist/src/cli-progress.js +59 -0
- package/dist/src/cli.js +14 -6
- package/dist/src/graph/attribution.d.ts +2 -2
- package/dist/src/graph/attribution.js +36 -13
- package/dist/src/graph/repository.d.ts +30 -3
- package/dist/src/graph/repository.js +396 -158
- package/dist/src/graph/repositoryScanWorker.d.ts +21 -0
- package/dist/src/graph/repositoryScanWorker.js +45 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +4 -0
- package/dist/src/mcp/server.js +16 -1
- package/dist/src/parser/treeSitter.js +20 -1
- package/dist/src/scanner/artifactInventory.d.ts +35 -0
- package/dist/src/scanner/artifactInventory.js +139 -0
- package/dist/src/scanner/fingerprint.js +5 -0
- package/dist/src/scanner/javaDependencyPlugin.d.ts +5 -0
- package/dist/src/scanner/javaDependencyPlugin.js +107 -0
- package/dist/src/scanner/javaPlugin.d.ts +5 -0
- package/dist/src/scanner/javaPlugin.js +199 -0
- package/dist/src/scanner/javaScanWorker.d.ts +2 -0
- package/dist/src/scanner/javaScanWorker.js +8 -0
- package/dist/src/scanner/packageParseWorker.d.ts +17 -0
- package/dist/src/scanner/packageParseWorker.js +30 -0
- package/dist/src/scanner/packagePlugin.js +83 -24
- package/dist/src/scanner/parallelScan.d.ts +2 -0
- package/dist/src/scanner/parallelScan.js +32 -0
- package/dist/src/scanner/plugins.d.ts +32 -2
- package/dist/src/scanner/plugins.js +51 -5
- package/dist/src/scanner/pythonPlugin.d.ts +5 -0
- package/dist/src/scanner/pythonPlugin.js +198 -0
- package/dist/src/scanner/pythonScanWorker.d.ts +2 -0
- package/dist/src/scanner/pythonScanWorker.js +8 -0
- package/dist/src/storage/connection.js +63 -0
- package/dist/src/storage/database.d.ts +1 -0
- package/dist/src/storage/database.js +1 -0
- package/dist/src/storage/graph-writes.d.ts +16 -3
- package/dist/src/storage/graph-writes.js +142 -17
- package/dist/src/storage/manifest-inventory.d.ts +23 -0
- package/dist/src/storage/manifest-inventory.js +82 -0
- package/dist/src/storage/queries.d.ts +6 -1
- package/dist/src/storage/queries.js +67 -1
- package/dist/src/storage/schema.d.ts +2 -2
- package/dist/src/storage/schema.js +44 -1
- package/dist/src/types.d.ts +102 -6
- package/dist/src/watch.d.ts +17 -2
- package/dist/src/watch.js +208 -46
- package/package.json +9 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { extractGraph } from './extract.js';
|
|
2
|
+
export interface RepositoryScanTask {
|
|
3
|
+
repositoryId: string;
|
|
4
|
+
relativePath: string;
|
|
5
|
+
absolutePath: string;
|
|
6
|
+
source: string;
|
|
7
|
+
sourceHash: string;
|
|
8
|
+
mtimeMs: number;
|
|
9
|
+
}
|
|
10
|
+
export interface RepositoryScanResult {
|
|
11
|
+
relativePath: string;
|
|
12
|
+
sourceHash: string;
|
|
13
|
+
size: number;
|
|
14
|
+
mtimeMs: number;
|
|
15
|
+
language?: string;
|
|
16
|
+
parser?: string;
|
|
17
|
+
parserVersion?: string;
|
|
18
|
+
graph?: ReturnType<typeof extractGraph>;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=repositoryScanWorker.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { parentPort } from 'node:worker_threads';
|
|
2
|
+
import { extractGraph, hashParts } from './extract.js';
|
|
3
|
+
import { parseSource } from '../parser/treeSitter.js';
|
|
4
|
+
if (!parentPort)
|
|
5
|
+
throw new Error('repository scan worker requires a parent port');
|
|
6
|
+
parentPort.on('message', (tasks) => {
|
|
7
|
+
const results = tasks.map((task) => {
|
|
8
|
+
const parsed = parseSource(task.absolutePath, task.source);
|
|
9
|
+
if (!parsed) {
|
|
10
|
+
return { relativePath: task.relativePath, sourceHash: task.sourceHash, size: Buffer.byteLength(task.source, 'utf8'), mtimeMs: task.mtimeMs, error: 'unsupported or unparseable source' };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return {
|
|
14
|
+
relativePath: task.relativePath,
|
|
15
|
+
sourceHash: task.sourceHash,
|
|
16
|
+
size: Buffer.byteLength(task.source, 'utf8'),
|
|
17
|
+
mtimeMs: task.mtimeMs,
|
|
18
|
+
language: parsed.language,
|
|
19
|
+
parser: parsed.parser,
|
|
20
|
+
parserVersion: parsed.parserVersion,
|
|
21
|
+
graph: parsed.language === 'python' || parsed.language === 'java'
|
|
22
|
+
? { nodes: [], edges: [] }
|
|
23
|
+
: extractGraph({
|
|
24
|
+
repositoryId: task.repositoryId,
|
|
25
|
+
fileId: hashParts([task.repositoryId, task.relativePath]),
|
|
26
|
+
relativePath: task.relativePath,
|
|
27
|
+
source: task.source,
|
|
28
|
+
sourceHash: task.sourceHash,
|
|
29
|
+
parseResult: parsed
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
relativePath: task.relativePath,
|
|
36
|
+
sourceHash: task.sourceHash,
|
|
37
|
+
size: Buffer.byteLength(task.source, 'utf8'),
|
|
38
|
+
mtimeMs: task.mtimeMs,
|
|
39
|
+
error: error instanceof Error ? error.message : String(error)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
parentPort.postMessage(results);
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=repositoryScanWorker.js.map
|
package/dist/src/index.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ export { watchRepository } from './watch.js';
|
|
|
3
3
|
export { SCANNER_PLUGIN_API_VERSION, DEFAULT_PLUGIN_RESOURCE_LIMITS, createRepositoryScanContext, runScannerPlugins, BUILTIN_GRAPH_PLUGIN } from './scanner/plugins.js';
|
|
4
4
|
export type { ScannerPlugin, ScannerCapability, PluginConflictPolicy, PluginNodeFact, PluginEdgeFact, PluginScanResult, PluginRunResult, PluginResourceLimits, RepositoryScanContext, RepositoryScanFile } from './scanner/plugins.js';
|
|
5
5
|
export { PackagePlugin } from './scanner/packagePlugin.js';
|
|
6
|
+
export { PythonScannerPlugin, PYTHON_SCANNER_PLUGIN_NAME, scanPythonFiles } from './scanner/pythonPlugin.js';
|
|
7
|
+
export { JavaScannerPlugin, JAVA_SCANNER_PLUGIN_NAME, scanJavaFiles } from './scanner/javaPlugin.js';
|
|
8
|
+
export { JavaDependencyPlugin, JAVA_DEPENDENCY_PLUGIN_NAME, scanJavaDependencies } from './scanner/javaDependencyPlugin.js';
|
|
9
|
+
export { inventoryRepositoryManifests, inspectRepositoryManifests, mergeManifestInventories } from './scanner/artifactInventory.js';
|
|
10
|
+
export type { RepositoryManifest, ManifestKind, ManifestStatus } from './scanner/artifactInventory.js';
|
|
6
11
|
export { viewGraph, loadViewSnapshot, buildViewModel, renderInteractiveHtml, renderSvgMarkup, openInDefaultBrowser, openHtmlArtifactInBrowser, writeTemporaryHtmlArtifact } from './view/index.js';
|
|
7
12
|
export { repoDatabasePath, homeDatabasePath, resolveProjectRoot } from './config/paths.js';
|
|
8
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.js
CHANGED
|
@@ -2,6 +2,10 @@ export { initGraph, listEdges, listNodes, graphStatus, callGraph, neighbors, con
|
|
|
2
2
|
export { watchRepository } from './watch.js';
|
|
3
3
|
export { SCANNER_PLUGIN_API_VERSION, DEFAULT_PLUGIN_RESOURCE_LIMITS, createRepositoryScanContext, runScannerPlugins, BUILTIN_GRAPH_PLUGIN } from './scanner/plugins.js';
|
|
4
4
|
export { PackagePlugin } from './scanner/packagePlugin.js';
|
|
5
|
+
export { PythonScannerPlugin, PYTHON_SCANNER_PLUGIN_NAME, scanPythonFiles } from './scanner/pythonPlugin.js';
|
|
6
|
+
export { JavaScannerPlugin, JAVA_SCANNER_PLUGIN_NAME, scanJavaFiles } from './scanner/javaPlugin.js';
|
|
7
|
+
export { JavaDependencyPlugin, JAVA_DEPENDENCY_PLUGIN_NAME, scanJavaDependencies } from './scanner/javaDependencyPlugin.js';
|
|
8
|
+
export { inventoryRepositoryManifests, inspectRepositoryManifests, mergeManifestInventories } from './scanner/artifactInventory.js';
|
|
5
9
|
export { viewGraph, loadViewSnapshot, buildViewModel, renderInteractiveHtml, renderSvgMarkup, openInDefaultBrowser, openHtmlArtifactInBrowser, writeTemporaryHtmlArtifact } from './view/index.js';
|
|
6
10
|
export { repoDatabasePath, homeDatabasePath, resolveProjectRoot } from './config/paths.js';
|
|
7
11
|
//# sourceMappingURL=index.js.map
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { graphStatus, listEdges, listNodes, neighbors, runReadOnlySql, scanRepository } from '../graph/repository.js';
|
|
4
|
+
import { contextGraph, graphStatus, listEdges, listNodes, neighbors, runReadOnlySql, scanRepository } from '../graph/repository.js';
|
|
5
5
|
const scopeSchema = z.enum(['repo', 'home', 'both']).default('repo');
|
|
6
6
|
export async function startMcpServer({ root = process.cwd() } = {}) {
|
|
7
7
|
const server = new McpServer({
|
|
@@ -13,6 +13,21 @@ export async function startMcpServer({ root = process.cwd() } = {}) {
|
|
|
13
13
|
root: z.string().optional().describe('Repository root. Defaults to the MCP server cwd.'),
|
|
14
14
|
scope: scopeSchema.describe('Graph storage scope to update.')
|
|
15
15
|
}, async (input) => jsonResponse(await scanRepository({ root: input.root ?? root, scope: input.scope })));
|
|
16
|
+
tool('context_graph_context', 'Return stored, relationship-aware context and its persisted freshness report without scanning.', {
|
|
17
|
+
root: z.string().optional(),
|
|
18
|
+
scope: scopeSchema,
|
|
19
|
+
symbol: z.string().min(1),
|
|
20
|
+
kind: z.enum(['file', 'symbol', 'import', 'export', 'call', 'user', 'package']).optional(),
|
|
21
|
+
depth: z.number().int().min(0).max(5).optional(),
|
|
22
|
+
limit: z.number().int().positive().max(500).optional()
|
|
23
|
+
}, async (input) => jsonResponse(await contextGraph({
|
|
24
|
+
root: input.root ?? root,
|
|
25
|
+
scope: input.scope,
|
|
26
|
+
symbol: input.symbol,
|
|
27
|
+
kind: input.kind,
|
|
28
|
+
depth: input.depth,
|
|
29
|
+
limit: input.limit
|
|
30
|
+
})));
|
|
16
31
|
tool('context_graph_status', 'Return repository graph counts and database paths.', {
|
|
17
32
|
root: z.string().optional(),
|
|
18
33
|
scope: scopeSchema
|
|
@@ -2,6 +2,8 @@ import path from 'node:path';
|
|
|
2
2
|
import Parser from 'tree-sitter';
|
|
3
3
|
import JavaScriptLanguage from 'tree-sitter-javascript';
|
|
4
4
|
import TypeScriptLanguages from 'tree-sitter-typescript';
|
|
5
|
+
import PythonLanguage from 'tree-sitter-python';
|
|
6
|
+
import JavaLanguage from 'tree-sitter-java';
|
|
5
7
|
const typeScriptModule = TypeScriptLanguages.default ?? TypeScriptLanguages;
|
|
6
8
|
const LANGUAGE_CONFIGS = [
|
|
7
9
|
{
|
|
@@ -21,6 +23,18 @@ const LANGUAGE_CONFIGS = [
|
|
|
21
23
|
parserVersion: 'tree-sitter-typescript-tsx',
|
|
22
24
|
extensions: ['.tsx'],
|
|
23
25
|
language: typeScriptModule.tsx ?? typeScriptModule.typescript ?? typeScriptModule
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'python',
|
|
29
|
+
parserVersion: 'tree-sitter-python',
|
|
30
|
+
extensions: ['.py'],
|
|
31
|
+
language: PythonLanguage.default ?? PythonLanguage
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'java',
|
|
35
|
+
parserVersion: 'tree-sitter-java@0.21.0',
|
|
36
|
+
extensions: ['.java'],
|
|
37
|
+
language: JavaLanguage.default ?? JavaLanguage
|
|
24
38
|
}
|
|
25
39
|
];
|
|
26
40
|
const byExtension = new Map();
|
|
@@ -30,6 +44,7 @@ for (const config of LANGUAGE_CONFIGS) {
|
|
|
30
44
|
}
|
|
31
45
|
}
|
|
32
46
|
const parserCache = new Map();
|
|
47
|
+
const MIN_PARSE_BUFFER_SIZE = 32 * 1024;
|
|
33
48
|
export function supportedExtensions() {
|
|
34
49
|
return [...byExtension.keys()];
|
|
35
50
|
}
|
|
@@ -51,7 +66,11 @@ export function parseSource(filePath, source) {
|
|
|
51
66
|
}
|
|
52
67
|
let tree;
|
|
53
68
|
try {
|
|
54
|
-
tree
|
|
69
|
+
// tree-sitter's native string input path rejects sources larger than its
|
|
70
|
+
// default 32 KiB input buffer. Give it enough room for this source while
|
|
71
|
+
// retaining the default-sized buffer for smaller files.
|
|
72
|
+
const bufferSize = Math.max(MIN_PARSE_BUFFER_SIZE, Buffer.byteLength(source, 'utf8') + 1);
|
|
73
|
+
tree = parser.parse(source, undefined, { bufferSize });
|
|
55
74
|
}
|
|
56
75
|
catch {
|
|
57
76
|
return null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type ManifestKind = 'maven-pom' | 'gradle-groovy' | 'gradle-kotlin';
|
|
2
|
+
export type ManifestStatus = 'observed' | 'read_failed' | 'traversal_incomplete' | 'confirmed_deleted';
|
|
3
|
+
/** The in-memory representation passed to repository analyzers. */
|
|
4
|
+
export interface RepositoryManifest {
|
|
5
|
+
path: string;
|
|
6
|
+
kind: ManifestKind;
|
|
7
|
+
moduleIdentity: string;
|
|
8
|
+
size: number;
|
|
9
|
+
mtimeMs: number;
|
|
10
|
+
sourceHash: string | null;
|
|
11
|
+
content: string | null;
|
|
12
|
+
status: ManifestStatus;
|
|
13
|
+
/** Set when this record came from the durable inventory. */
|
|
14
|
+
generation?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ManifestInventoryResult {
|
|
17
|
+
manifests: RepositoryManifest[];
|
|
18
|
+
/** False means the absence of a prior path must not be interpreted as deletion. */
|
|
19
|
+
complete: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const MAX_MANIFEST_BYTES: number;
|
|
22
|
+
export declare const MAX_MANIFESTS = 256;
|
|
23
|
+
export declare const MAX_MANIFEST_CONTENT_BYTES: number;
|
|
24
|
+
/**
|
|
25
|
+
* Return the bounded Maven/Gradle inventory used by repository analyzers.
|
|
26
|
+
* Only the repository root and its immediate child modules are considered;
|
|
27
|
+
* symlinks and deeper build trees are deliberately ignored.
|
|
28
|
+
*/
|
|
29
|
+
export declare function inventoryRepositoryManifests(root: string, previous?: readonly RepositoryManifest[]): RepositoryManifest[];
|
|
30
|
+
/** Perform one bounded filesystem traversal, without interpreting missing paths as deletions. */
|
|
31
|
+
export declare function inspectRepositoryManifests(root: string): ManifestInventoryResult;
|
|
32
|
+
/** Merge a current traversal with durable records, retaining uncertain paths. */
|
|
33
|
+
export declare function mergeManifestInventories(current: readonly RepositoryManifest[], previous: readonly RepositoryManifest[], complete: boolean): RepositoryManifest[];
|
|
34
|
+
export declare function kindForPath(relativePath: string): ManifestKind | undefined;
|
|
35
|
+
//# sourceMappingURL=artifactInventory.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstatSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const MAX_MANIFEST_BYTES = 1 * 1024 * 1024;
|
|
5
|
+
export const MAX_MANIFESTS = 256;
|
|
6
|
+
export const MAX_MANIFEST_CONTENT_BYTES = 16 * 1024 * 1024;
|
|
7
|
+
const IGNORED_MODULE_DIRECTORIES = new Set(['.git', '.sling', 'node_modules', 'dist', 'coverage', '.next', '.turbo', '.cache']);
|
|
8
|
+
/**
|
|
9
|
+
* Return the bounded Maven/Gradle inventory used by repository analyzers.
|
|
10
|
+
* Only the repository root and its immediate child modules are considered;
|
|
11
|
+
* symlinks and deeper build trees are deliberately ignored.
|
|
12
|
+
*/
|
|
13
|
+
export function inventoryRepositoryManifests(root, previous = []) {
|
|
14
|
+
const result = inspectRepositoryManifests(root);
|
|
15
|
+
return mergeManifestInventories(result.manifests, previous, result.complete);
|
|
16
|
+
}
|
|
17
|
+
/** Perform one bounded filesystem traversal, without interpreting missing paths as deletions. */
|
|
18
|
+
export function inspectRepositoryManifests(root) {
|
|
19
|
+
const candidates = new Set();
|
|
20
|
+
const addDirectory = (directory, relativeDirectory) => {
|
|
21
|
+
for (const name of ['pom.xml', 'build.gradle', 'build.gradle.kts']) {
|
|
22
|
+
const relative = relativeDirectory ? path.posix.join(relativeDirectory, name) : name;
|
|
23
|
+
candidates.add(relative);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
addDirectory(root, '');
|
|
27
|
+
let children;
|
|
28
|
+
try {
|
|
29
|
+
children = readdirSync(root, { withFileTypes: true });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { manifests: [], complete: false };
|
|
33
|
+
}
|
|
34
|
+
for (const entry of children) {
|
|
35
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || IGNORED_MODULE_DIRECTORIES.has(entry.name))
|
|
36
|
+
continue;
|
|
37
|
+
addDirectory(path.join(root, entry.name), entry.name);
|
|
38
|
+
}
|
|
39
|
+
const sortedCandidates = [...candidates].sort();
|
|
40
|
+
let complete = true;
|
|
41
|
+
const manifests = [];
|
|
42
|
+
const existingCandidates = [];
|
|
43
|
+
for (const relativePath of sortedCandidates) {
|
|
44
|
+
const absolute = path.join(root, relativePath);
|
|
45
|
+
const kind = kindForPath(relativePath);
|
|
46
|
+
if (!kind)
|
|
47
|
+
continue;
|
|
48
|
+
try {
|
|
49
|
+
// lstat is intentional: a symlink is never a supported artifact, even if
|
|
50
|
+
// its target happens to be a regular manifest.
|
|
51
|
+
const stat = lstatSync(absolute);
|
|
52
|
+
if (!stat.isSymbolicLink() && stat.isFile())
|
|
53
|
+
existingCandidates.push({ relativePath, kind, stat });
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// A path that is simply absent is not an artifact candidate. Other
|
|
57
|
+
// failures (permissions/races) are retained as uncertain reads.
|
|
58
|
+
if (error.code === 'ENOENT')
|
|
59
|
+
continue;
|
|
60
|
+
complete = false;
|
|
61
|
+
manifests.push(unavailableManifest(relativePath, kind, 'read_failed'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (existingCandidates.length > MAX_MANIFESTS)
|
|
65
|
+
complete = false;
|
|
66
|
+
let aggregate = 0;
|
|
67
|
+
for (const { relativePath, kind, stat } of existingCandidates.slice(0, MAX_MANIFESTS)) {
|
|
68
|
+
const absolute = path.join(root, relativePath);
|
|
69
|
+
const size = stat.size;
|
|
70
|
+
const mtimeMs = Math.round(stat.mtimeMs);
|
|
71
|
+
if (size > MAX_MANIFEST_BYTES || aggregate + size > MAX_MANIFEST_CONTENT_BYTES) {
|
|
72
|
+
manifests.push({ path: relativePath, kind, moduleIdentity: moduleIdentityFor(relativePath), size, mtimeMs, sourceHash: null, content: null, status: 'traversal_incomplete' });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(absolute, 'utf8');
|
|
77
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
78
|
+
let afterRead;
|
|
79
|
+
try {
|
|
80
|
+
afterRead = statSync(absolute);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
manifests.push(unavailableManifest(relativePath, kind, 'read_failed', size, mtimeMs));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (bytes > MAX_MANIFEST_BYTES || aggregate + bytes > MAX_MANIFEST_CONTENT_BYTES || afterRead.size !== size || Math.round(afterRead.mtimeMs) !== mtimeMs) {
|
|
87
|
+
manifests.push({ path: relativePath, kind, moduleIdentity: moduleIdentityFor(relativePath), size: afterRead.size, mtimeMs: Math.round(afterRead.mtimeMs), sourceHash: null, content: null, status: 'traversal_incomplete' });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
aggregate += bytes;
|
|
91
|
+
manifests.push({ path: relativePath, kind, moduleIdentity: moduleIdentityFor(relativePath), size, mtimeMs, sourceHash: sha256(content), content, status: 'observed' });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
manifests.push(unavailableManifest(relativePath, kind, 'read_failed', size, mtimeMs));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { manifests: manifests.sort((a, b) => a.path.localeCompare(b.path)), complete: complete && !manifests.some((manifest) => manifest.status === 'traversal_incomplete' || manifest.status === 'read_failed') };
|
|
98
|
+
}
|
|
99
|
+
/** Merge a current traversal with durable records, retaining uncertain paths. */
|
|
100
|
+
export function mergeManifestInventories(current, previous, complete) {
|
|
101
|
+
const merged = new Map(previous.map((manifest) => [manifest.path, { ...manifest, content: null }]));
|
|
102
|
+
for (const manifest of current)
|
|
103
|
+
merged.set(manifest.path, manifest);
|
|
104
|
+
if (complete) {
|
|
105
|
+
for (const [manifestPath, previousManifest] of merged) {
|
|
106
|
+
if (current.some((manifest) => manifest.path === manifestPath))
|
|
107
|
+
continue;
|
|
108
|
+
merged.set(manifestPath, { ...previousManifest, content: null, status: 'confirmed_deleted' });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
for (const [manifestPath, previousManifest] of merged) {
|
|
113
|
+
if (current.some((manifest) => manifest.path === manifestPath))
|
|
114
|
+
continue;
|
|
115
|
+
merged.set(manifestPath, { ...previousManifest, content: null, status: 'traversal_incomplete' });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...merged.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
119
|
+
}
|
|
120
|
+
export function kindForPath(relativePath) {
|
|
121
|
+
if (relativePath === 'pom.xml' || relativePath.endsWith('/pom.xml'))
|
|
122
|
+
return 'maven-pom';
|
|
123
|
+
if (relativePath.endsWith('/build.gradle') || relativePath === 'build.gradle')
|
|
124
|
+
return 'gradle-groovy';
|
|
125
|
+
if (relativePath.endsWith('/build.gradle.kts') || relativePath === 'build.gradle.kts')
|
|
126
|
+
return 'gradle-kotlin';
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
function moduleIdentityFor(relativePath) {
|
|
130
|
+
const directory = path.posix.dirname(relativePath);
|
|
131
|
+
return directory === '.' ? 'root' : directory;
|
|
132
|
+
}
|
|
133
|
+
function unavailableManifest(pathValue, kind, status, size = 0, mtimeMs = 0) {
|
|
134
|
+
return { path: pathValue, kind, moduleIdentity: moduleIdentityFor(pathValue), size, mtimeMs, sourceHash: null, content: null, status };
|
|
135
|
+
}
|
|
136
|
+
function sha256(value) {
|
|
137
|
+
return createHash('sha256').update(value).digest('hex');
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=artifactInventory.js.map
|
|
@@ -2,9 +2,11 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { discoverSourceFiles } from './discover.js';
|
|
5
|
+
import { inventoryRepositoryManifests } from './artifactInventory.js';
|
|
5
6
|
/** Fast metadata fingerprint shared by watch and context freshness checks. */
|
|
6
7
|
export function collectRepositoryFingerprint(root) {
|
|
7
8
|
const files = discoverSourceFiles(root);
|
|
9
|
+
const manifests = inventoryRepositoryManifests(root);
|
|
8
10
|
const hash = createHash('sha256');
|
|
9
11
|
for (const relativePath of files) {
|
|
10
12
|
const absolutePath = path.join(root, relativePath);
|
|
@@ -22,6 +24,9 @@ export function collectRepositoryFingerprint(root) {
|
|
|
22
24
|
hash.update('\0missing\0');
|
|
23
25
|
}
|
|
24
26
|
}
|
|
27
|
+
for (const manifest of manifests) {
|
|
28
|
+
hash.update(`manifest:${manifest.path}\0${manifest.size}\0${manifest.mtimeMs}\0${manifest.sourceHash ?? manifest.status}\0`);
|
|
29
|
+
}
|
|
25
30
|
return hash.digest('hex');
|
|
26
31
|
}
|
|
27
32
|
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PluginScanResult, ScannerPlugin, RepositoryScanContext } from './plugins.js';
|
|
2
|
+
export declare const JAVA_DEPENDENCY_PLUGIN_NAME = "java-dependency-plugin";
|
|
3
|
+
export declare const JavaDependencyPlugin: ScannerPlugin;
|
|
4
|
+
export declare function scanJavaDependencies(context: RepositoryScanContext): PluginScanResult;
|
|
5
|
+
//# sourceMappingURL=javaDependencyPlugin.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export const JAVA_DEPENDENCY_PLUGIN_NAME = 'java-dependency-plugin';
|
|
2
|
+
const MAX_DECLARATIONS = 10_000;
|
|
3
|
+
export const JavaDependencyPlugin = {
|
|
4
|
+
name: JAVA_DEPENDENCY_PLUGIN_NAME,
|
|
5
|
+
apiVersion: 1,
|
|
6
|
+
capabilities: ['repository-analyzer'],
|
|
7
|
+
scan: (context) => scanJavaDependencies(context),
|
|
8
|
+
scanIncremental: (context, _delta, previous) => scanJavaDependenciesIncremental(context, previous)
|
|
9
|
+
};
|
|
10
|
+
export function scanJavaDependencies(context) {
|
|
11
|
+
const nodes = [];
|
|
12
|
+
const diagnostics = [];
|
|
13
|
+
for (const manifest of context.manifests) {
|
|
14
|
+
if (manifest.status !== 'observed' || manifest.content === null) {
|
|
15
|
+
if (manifest.status !== 'confirmed_deleted')
|
|
16
|
+
diagnostics.push(`${manifest.path}: manifest read unavailable (${manifest.status})`);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const parsed = manifest.kind === 'maven-pom' ? parseMaven(manifest) : parseGradle(manifest);
|
|
20
|
+
nodes.push(...parsed.nodes);
|
|
21
|
+
diagnostics.push(...parsed.diagnostics);
|
|
22
|
+
}
|
|
23
|
+
return { nodes, edges: [], diagnostics };
|
|
24
|
+
}
|
|
25
|
+
function scanJavaDependenciesIncremental(context, previous) {
|
|
26
|
+
const result = scanJavaDependencies(context);
|
|
27
|
+
// Repository facts are cheap and bounded, so each incremental run replaces
|
|
28
|
+
// the complete analyzer contribution. This also handles manifest-only edits
|
|
29
|
+
// and the final manifest deletion after reopening the database.
|
|
30
|
+
if (!previous || result.diagnostics?.length) {
|
|
31
|
+
// A bounded/read/parse failure is not a confirmed deletion. Keep the
|
|
32
|
+
// previous repository contribution while exposing diagnostics.
|
|
33
|
+
return { ...result, retractions: { nodes: [], edges: [] } };
|
|
34
|
+
}
|
|
35
|
+
return { ...result, retractions: { nodes: previous.nodes.map((node) => String(node.metadata.factKey ?? '')).filter(Boolean), edges: previous.edges.map((edge) => String(edge.metadata.factKey ?? '')).filter(Boolean) } };
|
|
36
|
+
}
|
|
37
|
+
function parseMaven(manifest) {
|
|
38
|
+
const source = manifest.content ?? '';
|
|
39
|
+
const diagnostics = [];
|
|
40
|
+
if (/<!DOCTYPE|<!ENTITY/iu.test(source))
|
|
41
|
+
return { nodes: [], diagnostics: ['pom.xml rejected DTD/entity expansion'] };
|
|
42
|
+
if (!/<project\b/iu.test(source) || !/<\/project\s*>/iu.test(source))
|
|
43
|
+
return { nodes: [], diagnostics: [`${manifest.path}: malformed or unsupported Maven XML`] };
|
|
44
|
+
const properties = new Map();
|
|
45
|
+
const propertyBlock = /<properties\b[^>]*>([\s\S]*?)<\/properties>/iu.exec(source)?.[1] ?? '';
|
|
46
|
+
for (const match of propertyBlock.matchAll(/<([\w.-]+)\s*>([^<]{0,1000})<\/\1\s*>/gu))
|
|
47
|
+
properties.set(match[1], match[2].trim());
|
|
48
|
+
const nodes = [];
|
|
49
|
+
let occurrence = 0;
|
|
50
|
+
for (const match of source.matchAll(/<dependency\b[^>]*>([\s\S]*?)<\/dependency\s*>/giu)) {
|
|
51
|
+
if (occurrence >= MAX_DECLARATIONS) {
|
|
52
|
+
diagnostics.push(`${manifest.path}: dependency declaration limit exceeded`);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
const block = match[1];
|
|
56
|
+
const group = xmlTag(block, 'groupId');
|
|
57
|
+
const artifact = xmlTag(block, 'artifactId');
|
|
58
|
+
const version = resolveProperty(xmlTag(block, 'version'), properties);
|
|
59
|
+
const scope = xmlTag(block, 'scope') || 'compile';
|
|
60
|
+
if (!group || !artifact || !version) {
|
|
61
|
+
diagnostics.push(`${manifest.path}: unsupported or unresolved Maven dependency at declaration ${occurrence}`);
|
|
62
|
+
occurrence += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const coordinate = `${group}:${artifact}:${version}`;
|
|
66
|
+
nodes.push(dependencyNode(manifest, `${manifest.path}\u0000${manifest.moduleIdentity}\u0000${coordinate}\u0000${scope}\u0000${occurrence}`, coordinate, group, artifact, version, scope, occurrence));
|
|
67
|
+
occurrence += 1;
|
|
68
|
+
}
|
|
69
|
+
return { nodes, diagnostics };
|
|
70
|
+
}
|
|
71
|
+
function parseGradle(manifest) {
|
|
72
|
+
const source = manifest.content ?? '';
|
|
73
|
+
const diagnostics = [];
|
|
74
|
+
const nodes = [];
|
|
75
|
+
let occurrence = 0;
|
|
76
|
+
// Literal Groovy/Kotlin dependency calls only. No Gradle expressions are evaluated.
|
|
77
|
+
const pattern = /\b(api|implementation|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly|annotationProcessor|kapt|classpath)\b\s*(?:\(\s*)?['"]([^'"\n]+)['"]\s*\)?/gu;
|
|
78
|
+
for (const match of source.matchAll(pattern)) {
|
|
79
|
+
if (occurrence >= MAX_DECLARATIONS) {
|
|
80
|
+
diagnostics.push(`${manifest.path}: dependency declaration limit exceeded`);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const raw = match[2].trim();
|
|
84
|
+
const parts = raw.split(':');
|
|
85
|
+
const configuration = match[1].trim();
|
|
86
|
+
if (parts.length < 3 || parts.slice(0, 3).some((part) => !/^[\w.${}-]+$/u.test(part))) {
|
|
87
|
+
diagnostics.push(`${manifest.path}: unsupported Gradle dependency declaration ${raw.slice(0, 120)}`);
|
|
88
|
+
occurrence += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const [group, artifact, version] = parts;
|
|
92
|
+
const coordinate = `${group}:${artifact}:${version}`;
|
|
93
|
+
nodes.push(dependencyNode(manifest, `${manifest.path}\u0000${manifest.moduleIdentity}\u0000${coordinate}\u0000${configuration}\u0000${occurrence}`, coordinate, group, artifact, version, configuration, occurrence));
|
|
94
|
+
occurrence += 1;
|
|
95
|
+
}
|
|
96
|
+
if (/\b(?:project|platform|enforcedPlatform|libs\.|version\.catalog|\$\{)/u.test(source))
|
|
97
|
+
diagnostics.push(`${manifest.path}: dynamic or unsupported Gradle dependency expressions were ignored`);
|
|
98
|
+
return { nodes, diagnostics };
|
|
99
|
+
}
|
|
100
|
+
function dependencyNode(manifest, factKey, coordinate, group, artifact, version, scope, occurrence) {
|
|
101
|
+
return { factKey: `java-dependency:${factKey}`, kind: 'package', type: 'java-dependency', name: coordinate, metadata: { dependencyType: 'java-dependency', coordinate, group, artifact, version, scope, configuration: scope, declarationOccurrence: occurrence, manifestPath: manifest.path, manifestKind: manifest.kind, moduleIdentity: manifest.moduleIdentity, sourceHash: manifest.sourceHash } };
|
|
102
|
+
}
|
|
103
|
+
function xmlTag(source, tag) { return new RegExp(`<${tag}\\s*>([^<]{0,1000})<\\/${tag}\\s*>`, 'iu').exec(source)?.[1]?.trim(); }
|
|
104
|
+
function resolveProperty(value, properties) { if (!value)
|
|
105
|
+
return undefined; const match = /^\$\{([^}]+)\}$/u.exec(value); return match ? properties.get(match[1]) : value; }
|
|
106
|
+
function configurationAt(source, offset) { const line = source.slice(Math.max(0, source.lastIndexOf('\n', offset - 1) + 1), offset); return /\b(\w+)\s*$/u.exec(line)?.[1] ?? 'implementation'; }
|
|
107
|
+
//# sourceMappingURL=javaDependencyPlugin.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PluginScanResult, RepositoryScanFile, ScannerPlugin } from './plugins.js';
|
|
2
|
+
export declare const JAVA_SCANNER_PLUGIN_NAME = "java-scanner-plugin";
|
|
3
|
+
export declare function scanJavaFiles(files: readonly RepositoryScanFile[]): PluginScanResult;
|
|
4
|
+
export declare const JavaScannerPlugin: ScannerPlugin;
|
|
5
|
+
//# sourceMappingURL=javaPlugin.d.ts.map
|