@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
|
@@ -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,12 @@ 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 { AttributionPlugin, ATTRIBUTION_PLUGIN_NAME } from './scanner/attributionPlugin.js';
|
|
10
|
+
export { inventoryRepositoryManifests, inspectRepositoryManifests, mergeManifestInventories } from './scanner/artifactInventory.js';
|
|
11
|
+
export type { RepositoryManifest, ManifestKind, ManifestStatus } from './scanner/artifactInventory.js';
|
|
6
12
|
export { viewGraph, loadViewSnapshot, buildViewModel, renderInteractiveHtml, renderSvgMarkup, openInDefaultBrowser, openHtmlArtifactInBrowser, writeTemporaryHtmlArtifact } from './view/index.js';
|
|
7
13
|
export { repoDatabasePath, homeDatabasePath, resolveProjectRoot } from './config/paths.js';
|
|
8
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.js
CHANGED
|
@@ -2,6 +2,11 @@ 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 { AttributionPlugin, ATTRIBUTION_PLUGIN_NAME } from './scanner/attributionPlugin.js';
|
|
9
|
+
export { inventoryRepositoryManifests, inspectRepositoryManifests, mergeManifestInventories } from './scanner/artifactInventory.js';
|
|
5
10
|
export { viewGraph, loadViewSnapshot, buildViewModel, renderInteractiveHtml, renderSvgMarkup, openInDefaultBrowser, openHtmlArtifactInBrowser, writeTemporaryHtmlArtifact } from './view/index.js';
|
|
6
11
|
export { repoDatabasePath, homeDatabasePath, resolveProjectRoot } from './config/paths.js';
|
|
7
12
|
//# 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({
|
|
@@ -11,8 +11,24 @@ export async function startMcpServer({ root = process.cwd() } = {}) {
|
|
|
11
11
|
const tool = server.tool.bind(server);
|
|
12
12
|
tool('context_graph_scan', 'Parse repository files with Tree-sitter and write discovered graph nodes/edges to SQLite.', {
|
|
13
13
|
root: z.string().optional().describe('Repository root. Defaults to the MCP server cwd.'),
|
|
14
|
-
scope: scopeSchema.describe('Graph storage scope to update.')
|
|
15
|
-
|
|
14
|
+
scope: scopeSchema.describe('Graph storage scope to update.'),
|
|
15
|
+
attribution: z.boolean().default(false).describe('Enable optional Git user attribution enrichment.')
|
|
16
|
+
}, async (input) => jsonResponse(await scanRepository({ root: input.root ?? root, scope: input.scope, attribution: input.attribution })));
|
|
17
|
+
tool('context_graph_context', 'Return stored, relationship-aware context and its persisted freshness report without scanning.', {
|
|
18
|
+
root: z.string().optional(),
|
|
19
|
+
scope: scopeSchema,
|
|
20
|
+
symbol: z.string().min(1),
|
|
21
|
+
kind: z.enum(['file', 'symbol', 'import', 'export', 'call', 'user', 'package']).optional(),
|
|
22
|
+
depth: z.number().int().min(0).max(5).optional(),
|
|
23
|
+
limit: z.number().int().positive().max(500).optional()
|
|
24
|
+
}, async (input) => jsonResponse(await contextGraph({
|
|
25
|
+
root: input.root ?? root,
|
|
26
|
+
scope: input.scope,
|
|
27
|
+
symbol: input.symbol,
|
|
28
|
+
kind: input.kind,
|
|
29
|
+
depth: input.depth,
|
|
30
|
+
limit: input.limit
|
|
31
|
+
})));
|
|
16
32
|
tool('context_graph_status', 'Return repository graph counts and database paths.', {
|
|
17
33
|
root: z.string().optional(),
|
|
18
34
|
scope: scopeSchema
|
|
@@ -32,11 +48,13 @@ export async function startMcpServer({ root = process.cwd() } = {}) {
|
|
|
32
48
|
root: z.string().optional(),
|
|
33
49
|
scope: scopeSchema,
|
|
34
50
|
kind: z.enum(['contains', 'defines', 'imports', 'exports', 'calls', 'attributed_to', 'provides']).optional(),
|
|
51
|
+
type: z.string().min(1).optional(),
|
|
35
52
|
limit: z.number().int().positive().max(500).default(50)
|
|
36
53
|
}, async (input) => jsonResponse(listEdges({
|
|
37
54
|
root: input.root ?? root,
|
|
38
55
|
scope: input.scope,
|
|
39
56
|
kind: input.kind,
|
|
57
|
+
type: input.type,
|
|
40
58
|
limit: input.limit
|
|
41
59
|
})));
|
|
42
60
|
tool('context_graph_neighbors', 'Return nearby nodes around a graph node id.', {
|
|
@@ -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
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ScannerPlugin } from './plugins.js';
|
|
2
|
+
export declare const ATTRIBUTION_PLUGIN_NAME = "git-attribution-plugin";
|
|
3
|
+
/** Optional deferred Git attribution for structural graph nodes. */
|
|
4
|
+
export declare const AttributionPlugin: ScannerPlugin;
|
|
5
|
+
//# sourceMappingURL=attributionPlugin.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { buildFileAttribution } from '../graph/attribution.js';
|
|
2
|
+
export const ATTRIBUTION_PLUGIN_NAME = 'git-attribution-plugin';
|
|
3
|
+
/** Optional deferred Git attribution for structural graph nodes. */
|
|
4
|
+
export const AttributionPlugin = {
|
|
5
|
+
name: ATTRIBUTION_PLUGIN_NAME,
|
|
6
|
+
apiVersion: 1,
|
|
7
|
+
capabilities: ['enrichment'],
|
|
8
|
+
scan: () => ({ nodes: [], edges: [] }),
|
|
9
|
+
enrich: ({ repository, work, source, nodes }) => buildFileAttribution({
|
|
10
|
+
root: repository.root,
|
|
11
|
+
relativePath: work.path,
|
|
12
|
+
source,
|
|
13
|
+
nodes
|
|
14
|
+
})
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=attributionPlugin.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { isSupportedSourceFile } from '../parser/treeSitter.js';
|
|
5
4
|
const DEFAULT_IGNORES = new Set([
|
|
@@ -13,13 +12,14 @@ const DEFAULT_IGNORES = new Set([
|
|
|
13
12
|
]);
|
|
14
13
|
export function discoverSourceFiles(root, { ignores = DEFAULT_IGNORES } = {}) {
|
|
15
14
|
const files = [];
|
|
16
|
-
|
|
15
|
+
const gitignore = readGitignore(root);
|
|
16
|
+
walk(root, root, files, ignores, gitignore);
|
|
17
17
|
return files.sort();
|
|
18
18
|
}
|
|
19
|
-
function walk(root, current, files, ignores) {
|
|
19
|
+
function walk(root, current, files, ignores, gitignore) {
|
|
20
20
|
const entries = readdirSync(current, { withFileTypes: true });
|
|
21
21
|
const relativePaths = entries.map((entry) => path.relative(root, path.join(current, entry.name)));
|
|
22
|
-
const gitIgnored = ignoredByGit(
|
|
22
|
+
const gitIgnored = ignoredByGit(gitignore, relativePaths);
|
|
23
23
|
for (let index = 0; index < entries.length; index += 1) {
|
|
24
24
|
const entry = entries[index];
|
|
25
25
|
if (!entry)
|
|
@@ -31,7 +31,7 @@ function walk(root, current, files, ignores) {
|
|
|
31
31
|
if (!relativePath || gitIgnored.has(relativePath))
|
|
32
32
|
continue;
|
|
33
33
|
if (entry.isDirectory()) {
|
|
34
|
-
walk(root, fullPath, files, ignores);
|
|
34
|
+
walk(root, fullPath, files, ignores, gitignore);
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
37
|
if (!entry.isFile())
|
|
@@ -44,17 +44,90 @@ function walk(root, current, files, ignores) {
|
|
|
44
44
|
files.push(relativePath);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
function readGitignore(root) {
|
|
48
|
+
let contents;
|
|
49
|
+
try {
|
|
50
|
+
contents = readFileSync(path.join(root, '.gitignore'), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return contents
|
|
56
|
+
.split(/\r?\n/u)
|
|
57
|
+
.map((line) => line.trim())
|
|
58
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'))
|
|
59
|
+
.map((line) => {
|
|
60
|
+
const negated = line.startsWith('!');
|
|
61
|
+
const pattern = negated ? line.slice(1) : line;
|
|
62
|
+
return { expression: gitignorePatternToRegExp(pattern), negated };
|
|
54
63
|
});
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
}
|
|
65
|
+
function ignoredByGit(patterns, relativePaths) {
|
|
66
|
+
const ignored = new Set();
|
|
67
|
+
for (const relativePath of relativePaths) {
|
|
68
|
+
const normalizedPath = relativePath.split(path.sep).join('/');
|
|
69
|
+
let isIgnored = false;
|
|
70
|
+
for (const pattern of patterns) {
|
|
71
|
+
if (pattern.expression.test(normalizedPath))
|
|
72
|
+
isIgnored = !pattern.negated;
|
|
73
|
+
}
|
|
74
|
+
if (isIgnored)
|
|
75
|
+
ignored.add(relativePath);
|
|
76
|
+
}
|
|
77
|
+
return ignored;
|
|
78
|
+
}
|
|
79
|
+
function gitignorePatternToRegExp(pattern) {
|
|
80
|
+
const directoryOnly = pattern.endsWith('/');
|
|
81
|
+
const withoutTrailingSlash = directoryOnly ? pattern.slice(0, -1) : pattern;
|
|
82
|
+
const anchored = withoutTrailingSlash.startsWith('/');
|
|
83
|
+
const value = anchored ? withoutTrailingSlash.slice(1) : withoutTrailingSlash;
|
|
84
|
+
const source = globToRegExp(value);
|
|
85
|
+
// A pattern without a slash applies to a name at any depth. Patterns with
|
|
86
|
+
// a slash are relative to the repository root, as they are in gitignore.
|
|
87
|
+
if (!value.includes('/')) {
|
|
88
|
+
return new RegExp(`(?:^|/)${source}(?:$|/)`);
|
|
89
|
+
}
|
|
90
|
+
return new RegExp(`^${source}(?:$|/)`);
|
|
91
|
+
}
|
|
92
|
+
function globToRegExp(pattern) {
|
|
93
|
+
let result = '';
|
|
94
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
95
|
+
const character = pattern[index];
|
|
96
|
+
if (character === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
if (character === '*') {
|
|
99
|
+
if (pattern[index + 1] === '*') {
|
|
100
|
+
while (pattern[index + 1] === '*')
|
|
101
|
+
index += 1;
|
|
102
|
+
if (pattern[index + 1] === '/') {
|
|
103
|
+
index += 1;
|
|
104
|
+
result += '(?:.*/)?';
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
result += '.*';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
result += '[^/]*';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (character === '?') {
|
|
115
|
+
result += '[^/]';
|
|
116
|
+
}
|
|
117
|
+
else if (character === '[') {
|
|
118
|
+
const closing = pattern.indexOf(']', index + 1);
|
|
119
|
+
if (closing !== -1) {
|
|
120
|
+
result += pattern.slice(index, closing + 1);
|
|
121
|
+
index = closing;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
result += '\\[';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
result += /[\\^$+.()|{}]/u.test(character) ? `\\${character}` : character;
|
|
129
|
+
}
|
|
57
130
|
}
|
|
58
|
-
return
|
|
131
|
+
return result;
|
|
59
132
|
}
|
|
60
133
|
//# sourceMappingURL=discover.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
|