@psnext/lscg 0.1.2 → 0.1.4

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 (48) hide show
  1. package/README.md +69 -3
  2. package/dist/src/cli.js +68 -13
  3. package/dist/src/graph/repository.d.ts +4 -1
  4. package/dist/src/graph/repository.js +106 -6
  5. package/dist/src/index.d.ts +3 -0
  6. package/dist/src/index.js +2 -0
  7. package/dist/src/mcp/server.js +2 -2
  8. package/dist/src/scanner/discover.js +0 -2
  9. package/dist/src/scanner/packagePlugin.d.ts +3 -0
  10. package/dist/src/scanner/packagePlugin.js +168 -0
  11. package/dist/src/scanner/plugins.d.ts +86 -0
  12. package/dist/src/scanner/plugins.js +503 -0
  13. package/dist/src/storage/connection.d.ts +6 -0
  14. package/dist/src/storage/connection.js +133 -0
  15. package/dist/src/storage/context-queries.d.ts +20 -0
  16. package/dist/src/storage/context-queries.js +137 -0
  17. package/dist/src/storage/database.d.ts +12 -71
  18. package/dist/src/storage/database.js +12 -562
  19. package/dist/src/storage/graph-writes.d.ts +17 -0
  20. package/dist/src/storage/graph-writes.js +126 -0
  21. package/dist/src/storage/plugin-contributions.d.ts +15 -0
  22. package/dist/src/storage/plugin-contributions.js +28 -0
  23. package/dist/src/storage/plugin-graph.d.ts +6 -0
  24. package/dist/src/storage/plugin-graph.js +81 -0
  25. package/dist/src/storage/queries.d.ts +25 -0
  26. package/dist/src/storage/queries.js +129 -0
  27. package/dist/src/storage/row-decoders.d.ts +8 -0
  28. package/dist/src/storage/row-decoders.js +37 -0
  29. package/dist/src/storage/schema.d.ts +2 -2
  30. package/dist/src/storage/schema.js +13 -2
  31. package/dist/src/storage/traversal-queries.d.ts +15 -0
  32. package/dist/src/storage/traversal-queries.js +87 -0
  33. package/dist/src/types.d.ts +10 -4
  34. package/dist/src/view/index.d.ts +1 -1
  35. package/dist/src/view/index.js +2 -2
  36. package/dist/src/view/model.d.ts +2 -0
  37. package/dist/src/view/render.d.ts +5 -2
  38. package/dist/src/view/render.js +81 -13
  39. package/dist/src/view/templates/icons/call.svg +13 -0
  40. package/dist/src/view/templates/icons/export.svg +1 -0
  41. package/dist/src/view/templates/icons/file.svg +9 -0
  42. package/dist/src/view/templates/icons/import.svg +1 -0
  43. package/dist/src/view/templates/icons/package.svg +1 -0
  44. package/dist/src/view/templates/icons/symbol.svg +7 -0
  45. package/dist/src/view/templates/icons/user.svg +15 -0
  46. package/dist/src/view/templates/interactive.css +17 -11
  47. package/dist/src/view/templates/interactive.html +236 -52
  48. package/package.json +1 -1
@@ -0,0 +1,168 @@
1
+ import { builtinModules } from 'node:module';
2
+ import { extractGraph, hashParts } from '../graph/extract.js';
3
+ import { parseSource } from '../parser/treeSitter.js';
4
+ import { BUILTIN_GRAPH_PLUGIN } from './plugins.js';
5
+ // This plugin adds package-level relationships on top of the built-in source
6
+ // graph. Built-in extraction already creates file/import/symbol nodes; this
7
+ // plugin creates package nodes and connects them to the existing import nodes.
8
+ //
9
+ // The important identity rule for edges is:
10
+ // - package nodes use this plugin's fact-key namespace (`package:<name>`), and
11
+ // - built-in nodes are referenced by their final graph ID with
12
+ // `targetPlugin: BUILTIN_GRAPH_PLUGIN`.
13
+ //
14
+ // Keep that distinction in mind when repointing an edge to another existing
15
+ // node. Do not use the package node's eventual database ID as its fact key.
16
+ const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
17
+ const BUILTINS = new Set(builtinModules.flatMap((name) => [name, name.replace(/^node:/, ''), `node:${name.replace(/^node:/, '')}`]));
18
+ /**
19
+ * Converts an import specifier into its package root.
20
+ *
21
+ * Examples:
22
+ * react -> react
23
+ * lodash/map -> lodash
24
+ * @scope/library/tool -> @scope/library
25
+ * ./local-file -> undefined
26
+ * node:fs -> undefined
27
+ *
28
+ * Returning the root is what lets several imports share one package node.
29
+ */
30
+ function packageRoot(specifier) {
31
+ if (!specifier || specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#') || /^[a-z][a-z\d+.-]*:/i.test(specifier))
32
+ return undefined;
33
+ if (BUILTINS.has(specifier))
34
+ return undefined;
35
+ if (specifier.startsWith('@')) {
36
+ const parts = specifier.split('/');
37
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined;
38
+ }
39
+ return specifier.split('/')[0];
40
+ }
41
+ function isManifest(value) {
42
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
43
+ }
44
+ export const PackagePlugin = {
45
+ name: 'package-plugin',
46
+ apiVersion: 1,
47
+ capabilities: ['repository-analyzer'],
48
+ scan: (context) => {
49
+ let manifest;
50
+ try {
51
+ manifest = JSON.parse(context.readFile('package.json'));
52
+ }
53
+ catch (error) {
54
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
55
+ return { nodes: [], edges: [] };
56
+ }
57
+ throw new Error(`invalid package.json: ${error instanceof Error ? error.message : String(error)}`);
58
+ }
59
+ if (!isManifest(manifest))
60
+ throw new Error('invalid package.json: expected an object');
61
+ const diagnostics = [];
62
+ const declarations = new Map();
63
+ for (const section of DEPENDENCY_SECTIONS) {
64
+ const entries = manifest[section];
65
+ if (entries === undefined)
66
+ continue;
67
+ if (!isManifest(entries)) {
68
+ diagnostics.push(`${section} must be an object`);
69
+ continue;
70
+ }
71
+ for (const [name, version] of Object.entries(entries)) {
72
+ if (typeof version !== 'string') {
73
+ diagnostics.push(`${section}.${name} must be a string`);
74
+ continue;
75
+ }
76
+ const list = declarations.get(name) ?? [];
77
+ list.push({ section, version });
78
+ declarations.set(name, list);
79
+ }
80
+ }
81
+ // Re-run the same extraction used by the built-in scanner. The resulting
82
+ // import IDs are stable graph node IDs, so they can be used directly as
83
+ // targets when targetPlugin is BUILTIN_GRAPH_PLUGIN.
84
+ const imports = [];
85
+ const repositoryId = hashParts(['repository', context.repoPath]);
86
+ for (const file of context.files) {
87
+ const parsed = parseSource(file.absolutePath, file.source);
88
+ if (!parsed)
89
+ continue;
90
+ const extracted = extractGraph({
91
+ repositoryId,
92
+ fileId: hashParts([repositoryId, file.path]),
93
+ relativePath: file.path,
94
+ source: file.source,
95
+ sourceHash: file.hash,
96
+ parseResult: parsed
97
+ });
98
+ for (const node of extracted.nodes) {
99
+ if (node.kind !== 'import')
100
+ continue;
101
+ const module = typeof node.metadata.module === 'string' ? node.metadata.module : node.name;
102
+ if (module)
103
+ imports.push({ path: file.path, id: node.id, module });
104
+ }
105
+ }
106
+ // A package is represented once even when it is imported by many files or
107
+ // through many subpaths. `implied` records imports that are not declared in
108
+ // any dependency section of package.json.
109
+ const usedPackages = new Map();
110
+ for (const entry of imports) {
111
+ const root = packageRoot(entry.module);
112
+ if (!root)
113
+ continue;
114
+ if (!usedPackages.has(root))
115
+ usedPackages.set(root, { implied: !declarations.has(root) });
116
+ }
117
+ const packageNames = new Set(usedPackages.keys());
118
+ const nodes = [...packageNames].sort().map((name) => {
119
+ // This fact key is local to PackagePlugin. The scanner host later
120
+ // materializes it as hashParts(['plugin-node', 'package-plugin', factKey]).
121
+ const packageDeclarations = declarations.get(name) ?? [];
122
+ const implied = !declarations.has(name);
123
+ const versions = new Set(packageDeclarations.map((declaration) => declaration.version));
124
+ if (versions.size > 1)
125
+ diagnostics.push(`${name} has conflicting declared versions`);
126
+ return {
127
+ factKey: `package:${name}`,
128
+ kind: 'package',
129
+ type: 'package',
130
+ name,
131
+ metadata: {
132
+ manifest: 'package.json',
133
+ packageName: name,
134
+ implied,
135
+ declarations: packageDeclarations
136
+ }
137
+ };
138
+ });
139
+ const edges = imports.flatMap((entry) => {
140
+ const name = packageRoot(entry.module);
141
+ if (!name || !packageNames.has(name))
142
+ return [];
143
+ // The source is a node emitted by this plugin, so sourceFactKey uses
144
+ // PackagePlugin's namespace and can be resolved as `package:<name>`.
145
+ //
146
+ // The target is an existing node emitted by the built-in graph scanner.
147
+ // `entry.id` is that node's final graph ID, not a plugin fact key. The
148
+ // explicit built-in namespace tells the scanner host to use the ID
149
+ // directly instead of looking for a PackagePlugin node with that key.
150
+ //
151
+ // To repoint this edge to another existing built-in node, replace
152
+ // `targetFactKey` with that node's ID and keep
153
+ // `targetPlugin: BUILTIN_GRAPH_PLUGIN`. For a node from another plugin,
154
+ // use that plugin's name in targetPlugin and its factKey in targetFactKey.
155
+ return [{
156
+ factKey: `provides:${name}:${entry.path}:${entry.id}`,
157
+ sourceFactKey: `package:${name}`,
158
+ targetFactKey: entry.id,
159
+ targetPlugin: BUILTIN_GRAPH_PLUGIN,
160
+ filePath: entry.path,
161
+ kind: 'provides',
162
+ metadata: { module: entry.module, package: name, implied: !declarations.has(name) }
163
+ }];
164
+ });
165
+ return { nodes, edges, diagnostics };
166
+ }
167
+ };
168
+ //# sourceMappingURL=packagePlugin.js.map
@@ -0,0 +1,86 @@
1
+ import type { FileRecord, GraphEdge, GraphNode, GraphExtractionResult, Point } from '../types.js';
2
+ export declare const SCANNER_PLUGIN_API_VERSION: 1;
3
+ export type PluginConflictPolicy = 'merge' | 'replace' | 'reject';
4
+ export type ScannerCapability = 'file-scanner' | 'repository-analyzer';
5
+ export interface RepositoryScanFile extends FileRecord {
6
+ absolutePath: string;
7
+ source: string;
8
+ }
9
+ export interface RepositoryScanContext {
10
+ repoPath: string;
11
+ files: readonly RepositoryScanFile[];
12
+ discoveredPaths: readonly string[];
13
+ readFile(relativePath: string): string;
14
+ }
15
+ export interface PluginNodeFact {
16
+ factKey: string;
17
+ /** Internal owner assigned by the host; not part of plugin output. */
18
+ pluginName?: string;
19
+ filePath?: string;
20
+ kind: GraphNode['kind'];
21
+ type: string;
22
+ name?: string | null;
23
+ startByte?: number;
24
+ endByte?: number;
25
+ startPoint?: Point;
26
+ endPoint?: Point;
27
+ sourceHash?: string;
28
+ parser?: string;
29
+ parserVersion?: string;
30
+ metadata?: Record<string, unknown>;
31
+ conflict?: PluginConflictPolicy;
32
+ }
33
+ export interface PluginEdgeFact {
34
+ factKey: string;
35
+ /** Internal owner used to reconcile persisted edges per successful plugin. */
36
+ pluginName?: string;
37
+ sourceFactKey: string;
38
+ targetFactKey: string;
39
+ /** Optional cross-plugin namespace; omitted references use the edge's plugin. */
40
+ sourcePlugin?: string;
41
+ /** Optional cross-plugin namespace; omitted references use the edge's plugin. */
42
+ targetPlugin?: string;
43
+ filePath?: string;
44
+ kind: GraphEdge['kind'];
45
+ confidence?: number;
46
+ metadata?: Record<string, unknown>;
47
+ conflict?: PluginConflictPolicy;
48
+ }
49
+ export interface PluginScanResult {
50
+ nodes: PluginNodeFact[];
51
+ edges: PluginEdgeFact[];
52
+ diagnostics?: string[];
53
+ }
54
+ export interface PluginResourceLimits {
55
+ maxNodes: number;
56
+ maxEdges: number;
57
+ maxDiagnostics: number;
58
+ maxMetadataBytes: number;
59
+ maxSerializedOutputBytes: number;
60
+ maxDiagnosticMessageBytes: number;
61
+ }
62
+ export declare const DEFAULT_PLUGIN_RESOURCE_LIMITS: PluginResourceLimits;
63
+ /** Namespace used by plugins to target nodes produced by the built-in scanner. */
64
+ export declare const BUILTIN_GRAPH_PLUGIN = "__lscg_builtin__";
65
+ export interface ScannerPlugin {
66
+ name: string;
67
+ apiVersion: typeof SCANNER_PLUGIN_API_VERSION;
68
+ capabilities: readonly ScannerCapability[];
69
+ priority?: number;
70
+ initialize?(context: RepositoryScanContext): void | Promise<void>;
71
+ scan(context: RepositoryScanContext): PluginScanResult | Promise<PluginScanResult>;
72
+ finalize?(context: RepositoryScanContext): PluginScanResult | void | Promise<PluginScanResult | void>;
73
+ dispose?(context: RepositoryScanContext): void | Promise<void>;
74
+ }
75
+ export interface PluginRunResult extends GraphExtractionResult {
76
+ diagnostics: string[];
77
+ failedPlugins: string[];
78
+ successfulPlugins?: string[];
79
+ pluginContributions?: Map<string, {
80
+ nodes: GraphNode[];
81
+ edges: GraphEdge[];
82
+ }>;
83
+ }
84
+ export declare function createRepositoryScanContext(repoPath: string): RepositoryScanContext;
85
+ export declare function runScannerPlugins(context: RepositoryScanContext, plugins: readonly ScannerPlugin[], resourceLimits?: Partial<PluginResourceLimits>): Promise<PluginRunResult>;
86
+ //# sourceMappingURL=plugins.d.ts.map