@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.
Files changed (62) hide show
  1. package/README.md +117 -15
  2. package/dist/bin/lscg.js +0 -0
  3. package/dist/src/cli-progress.d.ts +7 -0
  4. package/dist/src/cli-progress.js +59 -0
  5. package/dist/src/cli.js +62 -9
  6. package/dist/src/explore/sigma-provider.d.ts +27 -0
  7. package/dist/src/explore/sigma-provider.js +87 -0
  8. package/dist/src/explore/sigma-render.d.ts +18 -0
  9. package/dist/src/explore/sigma-render.js +67 -0
  10. package/dist/src/graph/attribution.d.ts +2 -2
  11. package/dist/src/graph/attribution.js +36 -13
  12. package/dist/src/graph/explore.d.ts +20 -0
  13. package/dist/src/graph/explore.js +200 -0
  14. package/dist/src/graph/repository.d.ts +36 -4
  15. package/dist/src/graph/repository.js +443 -159
  16. package/dist/src/graph/repositoryScanWorker.d.ts +21 -0
  17. package/dist/src/graph/repositoryScanWorker.js +45 -0
  18. package/dist/src/index.d.ts +6 -0
  19. package/dist/src/index.js +5 -0
  20. package/dist/src/mcp/server.js +21 -3
  21. package/dist/src/parser/treeSitter.js +20 -1
  22. package/dist/src/scanner/artifactInventory.d.ts +35 -0
  23. package/dist/src/scanner/artifactInventory.js +139 -0
  24. package/dist/src/scanner/attributionPlugin.d.ts +5 -0
  25. package/dist/src/scanner/attributionPlugin.js +16 -0
  26. package/dist/src/scanner/discover.js +89 -16
  27. package/dist/src/scanner/fingerprint.js +5 -0
  28. package/dist/src/scanner/javaDependencyPlugin.d.ts +5 -0
  29. package/dist/src/scanner/javaDependencyPlugin.js +107 -0
  30. package/dist/src/scanner/javaPlugin.d.ts +5 -0
  31. package/dist/src/scanner/javaPlugin.js +199 -0
  32. package/dist/src/scanner/javaScanWorker.d.ts +2 -0
  33. package/dist/src/scanner/javaScanWorker.js +8 -0
  34. package/dist/src/scanner/packageParseWorker.d.ts +17 -0
  35. package/dist/src/scanner/packageParseWorker.js +30 -0
  36. package/dist/src/scanner/packagePlugin.js +83 -24
  37. package/dist/src/scanner/parallelScan.d.ts +2 -0
  38. package/dist/src/scanner/parallelScan.js +32 -0
  39. package/dist/src/scanner/plugins.d.ts +38 -4
  40. package/dist/src/scanner/plugins.js +58 -5
  41. package/dist/src/scanner/pythonPlugin.d.ts +5 -0
  42. package/dist/src/scanner/pythonPlugin.js +198 -0
  43. package/dist/src/scanner/pythonScanWorker.d.ts +2 -0
  44. package/dist/src/scanner/pythonScanWorker.js +8 -0
  45. package/dist/src/storage/connection.js +85 -0
  46. package/dist/src/storage/database.d.ts +1 -0
  47. package/dist/src/storage/database.js +1 -0
  48. package/dist/src/storage/explore-queries.d.ts +52 -0
  49. package/dist/src/storage/explore-queries.js +184 -0
  50. package/dist/src/storage/graph-writes.d.ts +22 -3
  51. package/dist/src/storage/graph-writes.js +167 -20
  52. package/dist/src/storage/manifest-inventory.d.ts +23 -0
  53. package/dist/src/storage/manifest-inventory.js +82 -0
  54. package/dist/src/storage/plugin-graph.js +3 -3
  55. package/dist/src/storage/queries.d.ts +10 -3
  56. package/dist/src/storage/queries.js +99 -24
  57. package/dist/src/storage/schema.d.ts +2 -2
  58. package/dist/src/storage/schema.js +48 -1
  59. package/dist/src/types.d.ts +110 -6
  60. package/dist/src/watch.d.ts +20 -2
  61. package/dist/src/watch.js +211 -46
  62. 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
@@ -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
@@ -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
- }, async (input) => jsonResponse(await scanRepository({ root: input.root ?? root, scope: input.scope })));
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 = parser.parse(source);
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
- walk(root, root, files, ignores);
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(root, relativePaths);
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 ignoredByGit(root, relativePaths) {
48
- if (relativePaths.length === 0)
49
- return new Set();
50
- const result = spawnSync('git', ['check-ignore', '--stdin', '-z'], {
51
- cwd: root,
52
- input: `${relativePaths.join('\0')}\0`,
53
- encoding: 'utf8'
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
- if (result.error || (result.status !== 0 && result.status !== 1)) {
56
- return new Set();
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 new Set(result.stdout.split('\0').filter(Boolean));
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