@psnext/lscg 0.1.1

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 (42) hide show
  1. package/README.md +143 -0
  2. package/dist/bin/lscg.d.ts +3 -0
  3. package/dist/bin/lscg.js +11 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +524 -0
  6. package/dist/src/config/paths.d.ts +12 -0
  7. package/dist/src/config/paths.js +54 -0
  8. package/dist/src/graph/attribution.d.ts +8 -0
  9. package/dist/src/graph/attribution.js +112 -0
  10. package/dist/src/graph/extract.d.ts +4 -0
  11. package/dist/src/graph/extract.js +199 -0
  12. package/dist/src/graph/repository.d.ts +93 -0
  13. package/dist/src/graph/repository.js +361 -0
  14. package/dist/src/index.d.ts +5 -0
  15. package/dist/src/index.js +5 -0
  16. package/dist/src/mcp/server.d.ts +6 -0
  17. package/dist/src/mcp/server.js +81 -0
  18. package/dist/src/parser/treeSitter.d.ts +13 -0
  19. package/dist/src/parser/treeSitter.js +61 -0
  20. package/dist/src/scanner/discover.d.ts +4 -0
  21. package/dist/src/scanner/discover.js +62 -0
  22. package/dist/src/scanner/fingerprint.d.ts +3 -0
  23. package/dist/src/scanner/fingerprint.js +27 -0
  24. package/dist/src/storage/database.d.ts +72 -0
  25. package/dist/src/storage/database.js +563 -0
  26. package/dist/src/storage/schema.d.ts +4 -0
  27. package/dist/src/storage/schema.js +93 -0
  28. package/dist/src/types.d.ts +233 -0
  29. package/dist/src/types.js +2 -0
  30. package/dist/src/view/index.d.ts +27 -0
  31. package/dist/src/view/index.js +42 -0
  32. package/dist/src/view/layout.d.ts +28 -0
  33. package/dist/src/view/layout.js +235 -0
  34. package/dist/src/view/model.d.ts +64 -0
  35. package/dist/src/view/model.js +396 -0
  36. package/dist/src/view/open.d.ts +15 -0
  37. package/dist/src/view/open.js +37 -0
  38. package/dist/src/view/render.d.ts +9 -0
  39. package/dist/src/view/render.js +321 -0
  40. package/dist/src/watch.d.ts +40 -0
  41. package/dist/src/watch.js +118 -0
  42. package/package.json +65 -0
@@ -0,0 +1,112 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ export function buildFileAttribution({ root, relativePath, source, nodes }) {
3
+ const blameLines = blameFile(root, relativePath);
4
+ if (!blameLines)
5
+ return null;
6
+ const lineStarts = buildLineStarts(source);
7
+ const contributorEmails = new Map();
8
+ const nodeAttributions = nodes.flatMap((node) => {
9
+ const span = lineSpanForNode(node, lineStarts);
10
+ const uniqueEmails = new Set();
11
+ let latest = null;
12
+ for (let lineNumber = span.startLine; lineNumber <= span.endLine; lineNumber += 1) {
13
+ const blame = blameLines[lineNumber];
14
+ if (!blame)
15
+ continue;
16
+ uniqueEmails.add(blame.email);
17
+ contributorEmails.set(blame.email, { email: blame.email });
18
+ if (!latest || blame.timestamp > latest.timestamp || (blame.timestamp === latest.timestamp && lineNumber > latest.lineNumber)) {
19
+ latest = { ...blame, lineNumber };
20
+ }
21
+ }
22
+ return [{
23
+ nodeId: node.id,
24
+ contributorEmails: [...uniqueEmails].sort(),
25
+ lastModifiedEmail: latest?.email ?? null
26
+ }];
27
+ });
28
+ return {
29
+ contributorEmails: [...contributorEmails.values()].sort((left, right) => left.email.localeCompare(right.email)),
30
+ nodeAttributions
31
+ };
32
+ }
33
+ function blameFile(root, relativePath) {
34
+ const result = spawnSync('git', ['-C', root, 'blame', '--follow', '--line-porcelain', '--', relativePath], {
35
+ encoding: 'utf8'
36
+ });
37
+ if (result.error || result.status !== 0 || !result.stdout.trim()) {
38
+ return null;
39
+ }
40
+ const lines = result.stdout.split(/\r?\n/);
41
+ const blameLines = [];
42
+ let currentEmail = null;
43
+ let currentTimestamp = 0;
44
+ for (const line of lines) {
45
+ if (!line)
46
+ continue;
47
+ if (isHeaderLine(line)) {
48
+ currentEmail = null;
49
+ currentTimestamp = 0;
50
+ continue;
51
+ }
52
+ if (line.startsWith('author-mail ')) {
53
+ currentEmail = normalizeEmail(line.slice('author-mail '.length));
54
+ continue;
55
+ }
56
+ if (line.startsWith('author-time ')) {
57
+ currentTimestamp = Number(line.slice('author-time '.length)) || 0;
58
+ continue;
59
+ }
60
+ if (line.startsWith('\t')) {
61
+ if (currentEmail) {
62
+ blameLines.push({ email: currentEmail, timestamp: currentTimestamp });
63
+ }
64
+ }
65
+ }
66
+ return blameLines.length > 0 ? blameLines : null;
67
+ }
68
+ function isHeaderLine(line) {
69
+ return /^[0-9a-f]{7,40} \d+ \d+(?: \d+)?$/.test(line);
70
+ }
71
+ function normalizeEmail(value) {
72
+ return value.trim().replace(/^<|>$/g, '').toLowerCase();
73
+ }
74
+ function buildLineStarts(source) {
75
+ const buffer = Buffer.from(source, 'utf8');
76
+ const starts = [0];
77
+ for (let index = 0; index < buffer.length; index += 1) {
78
+ if (buffer[index] === 0x0a && index + 1 < buffer.length) {
79
+ starts.push(index + 1);
80
+ }
81
+ }
82
+ return starts;
83
+ }
84
+ function lineSpanForNode(node, lineStarts) {
85
+ if (lineStarts.length === 0) {
86
+ return { startLine: 0, endLine: 0 };
87
+ }
88
+ const startLine = lineIndexForByte(lineStarts, node.startByte);
89
+ const endLine = lineIndexForByte(lineStarts, Math.max(node.startByte, node.endByte - 1));
90
+ return {
91
+ startLine,
92
+ endLine: Math.max(startLine, endLine)
93
+ };
94
+ }
95
+ function lineIndexForByte(lineStarts, byteIndex) {
96
+ let low = 0;
97
+ let high = lineStarts.length - 1;
98
+ let result = 0;
99
+ while (low <= high) {
100
+ const mid = Math.floor((low + high) / 2);
101
+ const lineStart = lineStarts[mid] ?? 0;
102
+ if (lineStart <= byteIndex) {
103
+ result = mid;
104
+ low = mid + 1;
105
+ }
106
+ else {
107
+ high = mid - 1;
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+ //# sourceMappingURL=attribution.js.map
@@ -0,0 +1,4 @@
1
+ import type { ExtractGraphInput, GraphExtractionResult } from '../types.js';
2
+ export declare function extractGraph({ repositoryId, fileId, relativePath, source, sourceHash, parseResult }: ExtractGraphInput): GraphExtractionResult;
3
+ export declare function hashParts(parts: Array<string | number>): string;
4
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1,199 @@
1
+ import { createHash } from 'node:crypto';
2
+ import Parser from 'tree-sitter';
3
+ const INTERESTING_TYPES = new Set([
4
+ 'class_declaration',
5
+ 'function_declaration',
6
+ 'method_definition',
7
+ 'variable_declarator',
8
+ 'import_statement',
9
+ 'export_statement',
10
+ 'call_expression'
11
+ ]);
12
+ export function extractGraph({ repositoryId, fileId, relativePath, source, sourceHash, parseResult }) {
13
+ const sourceBuffer = Buffer.from(source, 'utf8');
14
+ const nodes = [];
15
+ const edges = [];
16
+ const fileNode = makeNode({
17
+ repositoryId,
18
+ fileId,
19
+ relativePath,
20
+ kind: 'file',
21
+ type: 'file',
22
+ name: relativePath,
23
+ startByte: 0,
24
+ endByte: sourceBuffer.length,
25
+ startPoint: { row: 0, column: 0 },
26
+ endPoint: parseResult.tree.rootNode.endPosition,
27
+ sourceHash,
28
+ parser: parseResult.parser,
29
+ parserVersion: parseResult.parserVersion,
30
+ metadata: { language: parseResult.language }
31
+ });
32
+ nodes.push(fileNode);
33
+ walk(parseResult.tree.rootNode, []);
34
+ return { nodes, edges };
35
+ function walk(treeNode, ancestry) {
36
+ let currentGraphNode = null;
37
+ let nextAncestry = ancestry;
38
+ if (INTERESTING_TYPES.has(treeNode.type)) {
39
+ currentGraphNode = treeNodeToGraphNode(treeNode);
40
+ nodes.push(currentGraphNode);
41
+ const parentGraphNode = ancestry.at(-1) ?? fileNode;
42
+ const edgeKind = edgeKindFor(parentGraphNode, currentGraphNode);
43
+ edges.push(makeEdge(repositoryId, fileId, parentGraphNode.id, currentGraphNode.id, edgeKind, {
44
+ discoveredFrom: treeNode.type
45
+ }));
46
+ if (currentGraphNode.kind === 'import') {
47
+ edges.push(makeEdge(repositoryId, fileId, fileNode.id, currentGraphNode.id, 'imports', {
48
+ module: currentGraphNode.metadata.module ?? currentGraphNode.name
49
+ }));
50
+ }
51
+ if (currentGraphNode.kind === 'export') {
52
+ edges.push(makeEdge(repositoryId, fileId, fileNode.id, currentGraphNode.id, 'exports', {}));
53
+ }
54
+ if (currentGraphNode.kind === 'call') {
55
+ const caller = ancestry.findLast((node) => node.kind === 'symbol') ?? fileNode;
56
+ edges.push(makeEdge(repositoryId, fileId, caller.id, currentGraphNode.id, 'calls', {
57
+ callee: currentGraphNode.name
58
+ }));
59
+ }
60
+ nextAncestry = [...ancestry, currentGraphNode];
61
+ }
62
+ for (const child of treeNode.namedChildren) {
63
+ walk(child, nextAncestry);
64
+ }
65
+ }
66
+ function treeNodeToGraphNode(treeNode) {
67
+ const kind = kindForTreeNode(treeNode);
68
+ const name = nameForTreeNode(treeNode, sourceBuffer);
69
+ return makeNode({
70
+ repositoryId,
71
+ fileId,
72
+ relativePath,
73
+ kind,
74
+ type: treeNode.type,
75
+ name,
76
+ startByte: treeNode.startIndex,
77
+ endByte: treeNode.endIndex,
78
+ startPoint: treeNode.startPosition,
79
+ endPoint: treeNode.endPosition,
80
+ sourceHash,
81
+ parser: parseResult.parser,
82
+ parserVersion: parseResult.parserVersion,
83
+ metadata: metadataForTreeNode(treeNode, sourceBuffer)
84
+ });
85
+ }
86
+ }
87
+ function makeNode({ repositoryId, fileId, relativePath, kind, type, name, startByte, endByte, startPoint, endPoint, sourceHash, parser, parserVersion, metadata }) {
88
+ const id = hashParts([
89
+ repositoryId,
90
+ fileId,
91
+ relativePath,
92
+ parserVersion,
93
+ kind,
94
+ type,
95
+ name ?? '',
96
+ startByte,
97
+ endByte
98
+ ]);
99
+ return {
100
+ id,
101
+ kind,
102
+ type,
103
+ name,
104
+ startByte,
105
+ endByte,
106
+ startPoint,
107
+ endPoint,
108
+ sourceHash,
109
+ parser,
110
+ parserVersion,
111
+ metadata: {
112
+ ...metadata,
113
+ lineageKey: hashParts([repositoryId, relativePath, kind, type, name ?? ''])
114
+ },
115
+ lastModifiedUserId: null
116
+ };
117
+ }
118
+ function makeEdge(repositoryId, fileId, sourceId, targetId, kind, metadata) {
119
+ return {
120
+ id: hashParts([repositoryId, fileId, sourceId, targetId, kind, JSON.stringify(metadata ?? {})]),
121
+ sourceId,
122
+ targetId,
123
+ kind,
124
+ confidence: confidenceFor(kind),
125
+ metadata
126
+ };
127
+ }
128
+ function kindForTreeNode(node) {
129
+ if (node.type === 'import_statement')
130
+ return 'import';
131
+ if (node.type === 'export_statement')
132
+ return 'export';
133
+ if (node.type === 'call_expression')
134
+ return 'call';
135
+ return 'symbol';
136
+ }
137
+ function edgeKindFor(parentNode, childNode) {
138
+ if (parentNode.kind === 'file' && childNode.kind === 'symbol')
139
+ return 'defines';
140
+ if (childNode.kind === 'import')
141
+ return 'contains';
142
+ if (childNode.kind === 'export')
143
+ return 'contains';
144
+ if (childNode.kind === 'call')
145
+ return 'contains';
146
+ return 'contains';
147
+ }
148
+ function confidenceFor(kind) {
149
+ if (kind === 'calls' || kind === 'imports')
150
+ return 0.65;
151
+ return 1;
152
+ }
153
+ function nameForTreeNode(node, sourceBuffer) {
154
+ const namedField = node.childForFieldName('name');
155
+ if (namedField)
156
+ return textFor(namedField, sourceBuffer).trim();
157
+ if (node.type === 'import_statement') {
158
+ const source = node.childForFieldName('source');
159
+ return source ? stripQuotes(textFor(source, sourceBuffer).trim()) : textFor(node, sourceBuffer).trim().slice(0, 120);
160
+ }
161
+ if (node.type === 'call_expression') {
162
+ const fn = node.childForFieldName('function');
163
+ return fn ? textFor(fn, sourceBuffer).trim().slice(0, 120) : textFor(node, sourceBuffer).trim().slice(0, 120);
164
+ }
165
+ if (node.type === 'export_statement') {
166
+ return (textFor(node, sourceBuffer).trim().split('\n')[0] ?? '').slice(0, 120);
167
+ }
168
+ return null;
169
+ }
170
+ function metadataForTreeNode(node, sourceBuffer) {
171
+ if (node.type === 'import_statement') {
172
+ const source = node.childForFieldName('source');
173
+ return {
174
+ module: source ? stripQuotes(textFor(source, sourceBuffer).trim()) : null,
175
+ snippet: textFor(node, sourceBuffer).trim().slice(0, 240)
176
+ };
177
+ }
178
+ if (node.type === 'call_expression') {
179
+ const fn = node.childForFieldName('function');
180
+ return {
181
+ callee: fn ? textFor(fn, sourceBuffer).trim().slice(0, 120) : null,
182
+ snippet: textFor(node, sourceBuffer).trim().slice(0, 240)
183
+ };
184
+ }
185
+ return { snippet: textFor(node, sourceBuffer).trim().slice(0, 240) };
186
+ }
187
+ function textFor(node, sourceBuffer) {
188
+ return sourceBuffer.subarray(node.startIndex, node.endIndex).toString('utf8');
189
+ }
190
+ function stripQuotes(value) {
191
+ return value.replace(/^['\"]|['\"]$/g, '');
192
+ }
193
+ export function hashParts(parts) {
194
+ return createHash('sha256')
195
+ .update(parts.map((part) => String(part)).join('\0'))
196
+ .digest('hex')
197
+ .slice(0, 32);
198
+ }
199
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1,93 @@
1
+ import type { GraphEdgeRow, GraphNodeRow, GraphNodeTextRow, NeighborRow, CallGraphMatch, RepositoryRecord, ScanSummary, StatusResult, StorageScope, ContextGraphResult } from '../types.js';
2
+ export declare function repositoryForRoot(root?: string | undefined): RepositoryRecord;
3
+ export declare function initGraph({ root, scope }?: {
4
+ root?: string | undefined;
5
+ scope?: StorageScope | 'both' | undefined;
6
+ }): {
7
+ repository: RepositoryRecord;
8
+ initialized: Array<{
9
+ scope: StorageScope;
10
+ databasePath: string;
11
+ }>;
12
+ };
13
+ export declare function scanRepository({ root, scope }?: {
14
+ root?: string | undefined;
15
+ scope?: StorageScope | 'both' | undefined;
16
+ }): Promise<ScanSummary>;
17
+ export declare function graphStatus({ root, scope }?: {
18
+ root?: string | undefined;
19
+ scope?: StorageScope | 'both' | undefined;
20
+ }): Array<{
21
+ scope: StorageScope;
22
+ databasePath: string;
23
+ } & StatusResult>;
24
+ export declare function listNodes({ root, scope, kind, limit }?: {
25
+ root?: string | undefined;
26
+ scope?: StorageScope | 'both' | undefined;
27
+ kind?: string | undefined;
28
+ limit?: number | undefined;
29
+ }): Array<{
30
+ scope: StorageScope;
31
+ } & GraphNodeRow>;
32
+ export declare function listNodeText({ root, scope, kind, term, limit }?: {
33
+ root?: string | undefined;
34
+ scope?: StorageScope | 'both' | undefined;
35
+ kind?: string | undefined;
36
+ term?: string | undefined;
37
+ limit?: number | undefined;
38
+ }): Array<{
39
+ scope: StorageScope;
40
+ root: string;
41
+ } & GraphNodeTextRow>;
42
+ export declare function listEdges({ root, scope, kind, limit }?: {
43
+ root?: string | undefined;
44
+ scope?: StorageScope | 'both' | undefined;
45
+ kind?: string | undefined;
46
+ limit?: number | undefined;
47
+ }): Array<{
48
+ scope: StorageScope;
49
+ } & GraphEdgeRow>;
50
+ export declare function neighbors({ root, scope, nodeId, depth, limit }?: {
51
+ root?: string | undefined;
52
+ scope?: StorageScope | 'both' | undefined;
53
+ nodeId?: string | undefined;
54
+ depth?: number | undefined;
55
+ limit?: number | undefined;
56
+ }): Array<{
57
+ scope: StorageScope;
58
+ } & NeighborRow>;
59
+ export declare function callGraph({ root, scope, term, kind, depth, limit }?: {
60
+ root?: string | undefined;
61
+ scope?: StorageScope | 'both' | undefined;
62
+ term?: string | undefined;
63
+ kind?: string | undefined;
64
+ depth?: number | undefined;
65
+ limit?: number | undefined;
66
+ }): Array<{
67
+ scope: StorageScope;
68
+ } & CallGraphMatch>;
69
+ export interface ContextGraphOptions {
70
+ root?: string | undefined;
71
+ scope?: StorageScope | 'both' | undefined;
72
+ symbol: string;
73
+ kind?: string | undefined;
74
+ file?: string | undefined;
75
+ depth?: number | undefined;
76
+ limit?: number | undefined;
77
+ candidateLimit?: number | undefined;
78
+ excerptLines?: number | undefined;
79
+ excerptBytes?: number | undefined;
80
+ excerpts?: boolean | undefined;
81
+ }
82
+ export declare function contextGraph(options: ContextGraphOptions): Promise<ContextGraphResult>;
83
+ export declare function runReadOnlySql({ root, scope, sql, attachHome, limit }?: {
84
+ root?: string | undefined;
85
+ scope?: StorageScope | 'both' | undefined;
86
+ sql?: string | undefined;
87
+ attachHome?: boolean | undefined;
88
+ limit?: number | undefined;
89
+ }): Array<{
90
+ scope: StorageScope;
91
+ rows: unknown[];
92
+ }>;
93
+ //# sourceMappingURL=repository.d.ts.map