@grafema/util 0.3.16 → 0.3.18
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/dist/core/FileOverview.d.ts +12 -0
- package/dist/core/FileOverview.d.ts.map +1 -1
- package/dist/core/FileOverview.js +98 -2
- package/dist/core/FileOverview.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest/generator.d.ts +67 -0
- package/dist/manifest/generator.d.ts.map +1 -0
- package/dist/manifest/generator.js +390 -0
- package/dist/manifest/generator.js.map +1 -0
- package/dist/manifest/index.d.ts +5 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +3 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/resolver.d.ts +111 -0
- package/dist/manifest/resolver.d.ts.map +1 -0
- package/dist/manifest/resolver.js +210 -0
- package/dist/manifest/resolver.js.map +1 -0
- package/dist/manifest/types.d.ts +66 -0
- package/dist/manifest/types.d.ts.map +1 -0
- package/dist/manifest/types.js +12 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/notation/archetypes.d.ts.map +1 -1
- package/dist/notation/archetypes.js +1 -0
- package/dist/notation/archetypes.js.map +1 -1
- package/package.json +3 -3
- package/src/core/FileOverview.ts +104 -2
- package/src/index.ts +14 -0
- package/src/manifest/generator.ts +479 -0
- package/src/manifest/index.ts +16 -0
- package/src/manifest/resolver.ts +248 -0
- package/src/manifest/types.ts +101 -0
- package/src/notation/archetypes.ts +1 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ManifestGenerator — generates manifest.yaml from a Grafema graph.
|
|
3
|
+
*
|
|
4
|
+
* Reads MODULE, EXPORT, EXTERNAL_MODULE, FUNCTION, CLASS nodes from the graph
|
|
5
|
+
* and assembles a Manifest object describing the package's export surface.
|
|
6
|
+
*
|
|
7
|
+
* Effects are looked up from the effects-db (builtins + packages) and
|
|
8
|
+
* propagated transitively via the call graph.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
14
|
+
import type { GraphBackend, NodeRecord } from '@grafema/types';
|
|
15
|
+
import { GRAFEMA_VERSION } from '../version.js';
|
|
16
|
+
import type {
|
|
17
|
+
Manifest,
|
|
18
|
+
ManifestExport,
|
|
19
|
+
ManifestImport,
|
|
20
|
+
EffectType,
|
|
21
|
+
ExportKind,
|
|
22
|
+
FlowType,
|
|
23
|
+
} from './types.js';
|
|
24
|
+
|
|
25
|
+
const SCHEMA_VERSION = 1;
|
|
26
|
+
const VALID_EFFECTS: Set<string> = new Set([
|
|
27
|
+
'PURE', 'MUTATION', 'IO', 'THROW', 'ASYNC', 'NONDETERMINISTIC', 'UNKNOWN',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
interface EffectsDB {
|
|
31
|
+
runtimes: Record<string, Record<string, EffectType[]>>;
|
|
32
|
+
packages: Record<string, Record<string, EffectType[]>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface GeneratorOptions {
|
|
36
|
+
/** Package purl (e.g., "pkg:npm/@grafema/util@0.2.0") */
|
|
37
|
+
purl: string;
|
|
38
|
+
/** Path to effects-db directory */
|
|
39
|
+
effectsDbPath?: string;
|
|
40
|
+
/** Path to the .grafema directory */
|
|
41
|
+
grafemaDir: string;
|
|
42
|
+
/** Source type of analyzed code */
|
|
43
|
+
sourceType?: 'source' | 'compiled_js' | 'minified' | 'dts_only';
|
|
44
|
+
/** Files that belong to this package (prefix filter) */
|
|
45
|
+
packagePrefix?: string;
|
|
46
|
+
/** Graph-relative path to the entry file (e.g., "packages/util/src/index.ts").
|
|
47
|
+
* When set, collects exports via EXPORT → EXPORT_BINDING graph traversal,
|
|
48
|
+
* ensuring only the public API surface is included. */
|
|
49
|
+
entryFile?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ManifestGenerator {
|
|
53
|
+
private backend: GraphBackend;
|
|
54
|
+
private options: GeneratorOptions;
|
|
55
|
+
private effectsDb: EffectsDB;
|
|
56
|
+
private effectsCache: Map<string, EffectType[]> = new Map();
|
|
57
|
+
|
|
58
|
+
constructor(backend: GraphBackend, options: GeneratorOptions) {
|
|
59
|
+
this.backend = backend;
|
|
60
|
+
this.options = options;
|
|
61
|
+
this.effectsDb = { runtimes: {}, packages: {} };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async generate(): Promise<Manifest> {
|
|
65
|
+
this.loadEffectsDB();
|
|
66
|
+
|
|
67
|
+
const exports = await this.collectExports();
|
|
68
|
+
await this.enrichEffects(exports);
|
|
69
|
+
const imports = await this.collectImports();
|
|
70
|
+
const totalInternal = await this.countInternalSymbols();
|
|
71
|
+
|
|
72
|
+
const manifest: Manifest = {
|
|
73
|
+
schema_version: SCHEMA_VERSION,
|
|
74
|
+
analyzer_version: GRAFEMA_VERSION,
|
|
75
|
+
authored: false,
|
|
76
|
+
confidence: this.computeConfidence(exports),
|
|
77
|
+
generated: new Date().toISOString(),
|
|
78
|
+
|
|
79
|
+
package: {
|
|
80
|
+
purl: this.options.purl,
|
|
81
|
+
source_type: this.options.sourceType ?? 'source',
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
exports,
|
|
85
|
+
imports,
|
|
86
|
+
|
|
87
|
+
capabilities: {
|
|
88
|
+
total_exports: exports.length,
|
|
89
|
+
total_internal_symbols: totalInternal,
|
|
90
|
+
has_graph: existsSync(join(this.options.grafemaDir, 'graph.rfdb')),
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
access: {
|
|
94
|
+
local: './graph.rfdb',
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
language: 'typescript',
|
|
98
|
+
language_specific: {
|
|
99
|
+
module_system: 'esm',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return manifest;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Serialize manifest to YAML string */
|
|
107
|
+
static toYaml(manifest: Manifest): string {
|
|
108
|
+
return stringifyYaml(manifest, {
|
|
109
|
+
lineWidth: 120,
|
|
110
|
+
defaultKeyType: 'PLAIN',
|
|
111
|
+
defaultStringType: 'PLAIN',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Effects DB loading ─────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
private loadEffectsDB(): void {
|
|
118
|
+
const dbPath = this.options.effectsDbPath;
|
|
119
|
+
if (!dbPath) return;
|
|
120
|
+
|
|
121
|
+
// Load runtime builtins
|
|
122
|
+
const nodeYaml = join(dbPath, 'runtimes', 'node.yaml');
|
|
123
|
+
if (existsSync(nodeYaml)) {
|
|
124
|
+
const raw = parseYaml(readFileSync(nodeYaml, 'utf-8')) as Record<string, Record<string, EffectType[]>>;
|
|
125
|
+
this.effectsDb.runtimes = raw;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Load package effects
|
|
129
|
+
const pkgDir = join(dbPath, 'packages');
|
|
130
|
+
if (existsSync(pkgDir)) {
|
|
131
|
+
for (const file of readdirSync(pkgDir)) {
|
|
132
|
+
if (!file.endsWith('.yaml')) continue;
|
|
133
|
+
const raw = parseYaml(readFileSync(join(pkgDir, file), 'utf-8'));
|
|
134
|
+
if (raw && typeof raw === 'object') {
|
|
135
|
+
for (const [pkg, fns] of Object.entries(raw)) {
|
|
136
|
+
this.effectsDb.packages[pkg] = fns as Record<string, EffectType[]>;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Look up effects for a builtin or external function */
|
|
144
|
+
private lookupEffects(module: string, fn: string): EffectType[] | null {
|
|
145
|
+
// Check runtime builtins (node:fs → readFileSync)
|
|
146
|
+
const nodeModule = `node:${module}`;
|
|
147
|
+
if (this.effectsDb.runtimes[nodeModule]?.[fn]) {
|
|
148
|
+
return this.effectsDb.runtimes[nodeModule][fn];
|
|
149
|
+
}
|
|
150
|
+
// Also check without node: prefix
|
|
151
|
+
if (this.effectsDb.runtimes[module]?.[fn]) {
|
|
152
|
+
return this.effectsDb.runtimes[module][fn];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check package effects
|
|
156
|
+
if (this.effectsDb.packages[module]?.[fn]) {
|
|
157
|
+
return this.effectsDb.packages[module][fn];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Export collection ──────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/** Definition node types we look up for exported symbols */
|
|
166
|
+
private static readonly DEF_TYPES = ['FUNCTION', 'CLASS', 'CONSTANT', 'INTERFACE'] as const;
|
|
167
|
+
|
|
168
|
+
private async collectExports(): Promise<ManifestExport[]> {
|
|
169
|
+
const seen = new Set<string>();
|
|
170
|
+
const exports: ManifestExport[] = [];
|
|
171
|
+
const entryFile = this.options.entryFile;
|
|
172
|
+
|
|
173
|
+
if (entryFile) {
|
|
174
|
+
// Graph-based approach:
|
|
175
|
+
// EXPORT(named) --EXPORTS--> EXPORT_BINDING(name, source) --> definition in source file
|
|
176
|
+
await this.collectExportsViaBindings(entryFile, exports, seen, new Set());
|
|
177
|
+
} else {
|
|
178
|
+
// Fallback: all exported definitions from the package
|
|
179
|
+
const prefix = this.options.packagePrefix ?? '';
|
|
180
|
+
for (const type of ManifestGenerator.DEF_TYPES) {
|
|
181
|
+
for await (const node of this.backend.queryNodes({ type: type as never })) {
|
|
182
|
+
if (prefix && !node.file?.startsWith(prefix)) continue;
|
|
183
|
+
if (!node.exported) continue;
|
|
184
|
+
if (!node.name || node.name.startsWith('<')) continue;
|
|
185
|
+
if (seen.has(node.name)) continue;
|
|
186
|
+
seen.add(node.name);
|
|
187
|
+
await this.addExportFromDefinition(node, exports);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
exports.sort((a, b) => a.name.localeCompare(b.name));
|
|
193
|
+
return exports;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Collect exports by traversing EXPORT → EXPORT_BINDING graph edges from entry file */
|
|
197
|
+
private async collectExportsViaBindings(
|
|
198
|
+
file: string,
|
|
199
|
+
exports: ManifestExport[],
|
|
200
|
+
seen: Set<string>,
|
|
201
|
+
visitedFiles: Set<string>,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
if (visitedFiles.has(file)) return;
|
|
204
|
+
visitedFiles.add(file);
|
|
205
|
+
|
|
206
|
+
for await (const exportNode of this.backend.queryNodes({ type: 'EXPORT' as never, file })) {
|
|
207
|
+
// Star re-exports: follow RE_EXPORTS edge to source module
|
|
208
|
+
if (exportNode.name?.startsWith('*:') || exportNode.name === '*') {
|
|
209
|
+
const reExportEdges = await this.backend.getOutgoingEdges(exportNode.id, ['RE_EXPORTS' as never]);
|
|
210
|
+
for (const edge of reExportEdges) {
|
|
211
|
+
const targetModule = await this.backend.getNode(edge.dst);
|
|
212
|
+
if (targetModule?.file) {
|
|
213
|
+
await this.collectExportsViaBindings(targetModule.file, exports, seen, visitedFiles);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Named exports: follow EXPORTS edges to EXPORT_BINDING nodes
|
|
220
|
+
const bindingEdges = await this.backend.getOutgoingEdges(exportNode.id, ['EXPORTS' as never]);
|
|
221
|
+
for (const edge of bindingEdges) {
|
|
222
|
+
const binding = await this.backend.getNode(edge.dst);
|
|
223
|
+
if (!binding?.name || seen.has(binding.name)) continue;
|
|
224
|
+
seen.add(binding.name);
|
|
225
|
+
|
|
226
|
+
// Resolve the definition node from the source file
|
|
227
|
+
const source = (binding as Record<string, unknown>).source as string | undefined;
|
|
228
|
+
const defNode = source
|
|
229
|
+
? await this.findDefinition(binding.name, source, file)
|
|
230
|
+
: await this.findDefinitionInFile(binding.name, file);
|
|
231
|
+
|
|
232
|
+
if (defNode) {
|
|
233
|
+
await this.addExportFromDefinition(defNode, exports);
|
|
234
|
+
} else {
|
|
235
|
+
// Type-only export or unresolvable — add as TYPE/VARIABLE
|
|
236
|
+
exports.push({
|
|
237
|
+
name: binding.name,
|
|
238
|
+
kind: 'TYPE',
|
|
239
|
+
semanticId: binding.id,
|
|
240
|
+
effects: ['PURE'],
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Find a definition node by name, resolving source path relative to the importing file.
|
|
248
|
+
* Follows barrel re-export chains: if the source file is a barrel (index.ts),
|
|
249
|
+
* looks for EXPORT_BINDING nodes there and follows their source recursively. */
|
|
250
|
+
private async findDefinition(
|
|
251
|
+
name: string, source: string, fromFile: string, depth = 0,
|
|
252
|
+
): Promise<NodeRecord | null> {
|
|
253
|
+
if (depth > 5) return null;
|
|
254
|
+
|
|
255
|
+
const resolved = this.resolveSourcePath(source, fromFile);
|
|
256
|
+
|
|
257
|
+
// Direct definition in resolved file
|
|
258
|
+
const def = await this.findDefinitionInFile(name, resolved);
|
|
259
|
+
if (def) return def;
|
|
260
|
+
|
|
261
|
+
// Follow barrel re-export chain via EXPORT_BINDING
|
|
262
|
+
for await (const binding of this.backend.queryNodes({
|
|
263
|
+
type: 'EXPORT_BINDING' as never, name, file: resolved,
|
|
264
|
+
})) {
|
|
265
|
+
const nextSource = (binding as Record<string, unknown>).source as string | undefined;
|
|
266
|
+
if (nextSource) {
|
|
267
|
+
return this.findDefinition(name, nextSource, resolved, depth + 1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Resolve a relative source path to a graph-relative file path */
|
|
275
|
+
private resolveSourcePath(source: string, fromFile: string): string {
|
|
276
|
+
const dir = fromFile.substring(0, fromFile.lastIndexOf('/'));
|
|
277
|
+
let resolved = source.startsWith('.') ? `${dir}/${source}` : source;
|
|
278
|
+
resolved = resolved.replace(/\/\.\//g, '/');
|
|
279
|
+
if (resolved.endsWith('.js')) {
|
|
280
|
+
resolved = resolved.replace(/\.js$/, '.ts');
|
|
281
|
+
}
|
|
282
|
+
return resolved;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Find a FUNCTION/CLASS/CONSTANT/INTERFACE node by name in a specific file */
|
|
286
|
+
private async findDefinitionInFile(name: string, file: string): Promise<NodeRecord | null> {
|
|
287
|
+
for (const type of ManifestGenerator.DEF_TYPES) {
|
|
288
|
+
for await (const node of this.backend.queryNodes({ type: type as never, name, file })) {
|
|
289
|
+
return node;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Build a ManifestExport entry from a definition node */
|
|
296
|
+
private async addExportFromDefinition(node: NodeRecord, exports: ManifestExport[]): Promise<void> {
|
|
297
|
+
const kind = this.nodeTypeToExportKind(node.type);
|
|
298
|
+
const entry: ManifestExport = {
|
|
299
|
+
name: node.name ?? '<unknown>',
|
|
300
|
+
kind,
|
|
301
|
+
semanticId: node.id,
|
|
302
|
+
effects: ['PURE'],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (kind === 'FUNCTION' || kind === 'CLASS') {
|
|
306
|
+
const params = (node as Record<string, unknown>).params as string[] | undefined;
|
|
307
|
+
if (params && params.length > 0) {
|
|
308
|
+
entry.params = params.map(p => ({ name: p, flow: 'IN' as FlowType }));
|
|
309
|
+
}
|
|
310
|
+
entry.returns = { flow: 'OUT' };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
exports.push(entry);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Map node type → ExportKind */
|
|
317
|
+
private nodeTypeToExportKind(type: string): ExportKind {
|
|
318
|
+
switch (type) {
|
|
319
|
+
case 'FUNCTION': return 'FUNCTION';
|
|
320
|
+
case 'CLASS': return 'CLASS';
|
|
321
|
+
case 'CONSTANT': return 'CONSTANT';
|
|
322
|
+
case 'INTERFACE': return 'INTERFACE';
|
|
323
|
+
default: return 'VARIABLE';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
// ── Effect computation ─────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
/** Enrich all exports with computed effects (transitive call graph analysis) */
|
|
331
|
+
private async enrichEffects(exports: ManifestExport[]): Promise<void> {
|
|
332
|
+
for (const entry of exports) {
|
|
333
|
+
if (entry.kind !== 'FUNCTION' && entry.kind !== 'CLASS') continue;
|
|
334
|
+
|
|
335
|
+
const effects = new Set<EffectType>();
|
|
336
|
+
const visited = new Set<string>();
|
|
337
|
+
await this.collectEffectsTransitively(entry.semanticId, effects, visited);
|
|
338
|
+
|
|
339
|
+
const computed = [...effects].filter(e => e !== 'PURE');
|
|
340
|
+
if (computed.length > 0) {
|
|
341
|
+
entry.effects = computed;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async collectEffectsTransitively(
|
|
347
|
+
nodeId: string,
|
|
348
|
+
effects: Set<EffectType>,
|
|
349
|
+
visited: Set<string>,
|
|
350
|
+
depth = 0
|
|
351
|
+
): Promise<void> {
|
|
352
|
+
if (visited.has(nodeId) || depth > 20) return;
|
|
353
|
+
visited.add(nodeId);
|
|
354
|
+
|
|
355
|
+
// Get outgoing CALLS edges
|
|
356
|
+
const callEdges = await this.backend.getOutgoingEdges(nodeId, ['CALLS' as never]);
|
|
357
|
+
|
|
358
|
+
for (const edge of callEdges) {
|
|
359
|
+
const targetNode = await this.backend.getNode(edge.dst);
|
|
360
|
+
if (!targetNode) continue;
|
|
361
|
+
|
|
362
|
+
// Check if target is an external call → look up in effects-db
|
|
363
|
+
if (targetNode.type === 'EXTERNAL_MODULE' || targetNode.type === 'GLOBAL_DEFINITION') {
|
|
364
|
+
const [module, fn] = this.parseCallTarget(targetNode);
|
|
365
|
+
if (module && fn) {
|
|
366
|
+
const knownEffects = this.lookupEffects(module, fn);
|
|
367
|
+
if (knownEffects) {
|
|
368
|
+
for (const e of knownEffects) {
|
|
369
|
+
if (VALID_EFFECTS.has(e) && e !== 'PURE') effects.add(e);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
effects.add('UNKNOWN');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Recurse into internal calls
|
|
379
|
+
await this.collectEffectsTransitively(edge.dst, effects, visited, depth + 1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check node's own properties for async/throw
|
|
383
|
+
const node = await this.backend.getNode(nodeId);
|
|
384
|
+
if (node) {
|
|
385
|
+
const meta = node as Record<string, unknown>;
|
|
386
|
+
if (meta.async === true) effects.add('ASYNC');
|
|
387
|
+
const cf = meta.controlFlow as Record<string, boolean> | undefined;
|
|
388
|
+
if (cf?.hasThrow) effects.add('THROW');
|
|
389
|
+
if (cf?.canReject) effects.add('THROW');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private parseCallTarget(node: NodeRecord): [string | null, string | null] {
|
|
394
|
+
// Parse semantic ID like "EXTERNAL_MODULE:fs" or call name like "fs.readFileSync"
|
|
395
|
+
const name = node.name ?? '';
|
|
396
|
+
if (name.includes('.')) {
|
|
397
|
+
const parts = name.split('.');
|
|
398
|
+
return [parts[0], parts.slice(1).join('.')];
|
|
399
|
+
}
|
|
400
|
+
return [name, null];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── Import collection ──────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
private async collectImports(): Promise<ManifestImport[]> {
|
|
406
|
+
const importMap = new Map<string, Set<string>>();
|
|
407
|
+
const prefix = this.options.packagePrefix ?? '';
|
|
408
|
+
|
|
409
|
+
for await (const node of this.backend.queryNodes({ type: 'IMPORT' as never })) {
|
|
410
|
+
if (prefix && !node.file?.startsWith(prefix)) continue;
|
|
411
|
+
|
|
412
|
+
const source = (node as Record<string, unknown>).source as string;
|
|
413
|
+
if (!source) continue;
|
|
414
|
+
|
|
415
|
+
// Skip relative imports (internal modules)
|
|
416
|
+
if (source.startsWith('.') || source.startsWith('/')) continue;
|
|
417
|
+
|
|
418
|
+
const purl = this.sourceToPurl(source);
|
|
419
|
+
if (!importMap.has(purl)) {
|
|
420
|
+
importMap.set(purl, new Set());
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const importName = node.name ?? '*';
|
|
424
|
+
importMap.get(purl)!.add(importName);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return [...importMap.entries()]
|
|
428
|
+
.map(([purl, symbols]) => ({
|
|
429
|
+
purl,
|
|
430
|
+
symbols: [...symbols].sort(),
|
|
431
|
+
}))
|
|
432
|
+
.sort((a, b) => a.purl.localeCompare(b.purl));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private sourceToPurl(source: string): string {
|
|
436
|
+
// Node builtins: node:fs, node:path, etc.
|
|
437
|
+
if (source.startsWith('node:')) {
|
|
438
|
+
const mod = source.replace('node:', '');
|
|
439
|
+
return `pkg:npm/node@*#${mod}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Scoped packages: @scope/name or @scope/name/path → pkg:npm/@scope/name
|
|
443
|
+
if (source.startsWith('@')) {
|
|
444
|
+
const parts = source.split('/');
|
|
445
|
+
const pkg = `${parts[0]}/${parts[1]}`;
|
|
446
|
+
return `pkg:npm/${pkg}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Regular packages: name or name/path → pkg:npm/name
|
|
450
|
+
const pkg = source.split('/')[0];
|
|
451
|
+
return `pkg:npm/${pkg}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Helpers ────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
private async countInternalSymbols(): Promise<number> {
|
|
457
|
+
const prefix = this.options.packagePrefix ?? '';
|
|
458
|
+
let count = 0;
|
|
459
|
+
for (const type of ['FUNCTION', 'CLASS', 'VARIABLE', 'CONSTANT', 'INTERFACE']) {
|
|
460
|
+
for await (const node of this.backend.queryNodes({ type: type as never })) {
|
|
461
|
+
if (prefix && !node.file?.startsWith(prefix)) continue;
|
|
462
|
+
count++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return count;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private computeConfidence(exports: ManifestExport[]): number {
|
|
469
|
+
if (exports.length === 0) return 0;
|
|
470
|
+
|
|
471
|
+
const unknownCount = exports.filter(e =>
|
|
472
|
+
e.effects.includes('UNKNOWN')
|
|
473
|
+
).length;
|
|
474
|
+
|
|
475
|
+
// confidence = 1.0 - (unknown_ratio * 0.5)
|
|
476
|
+
const unknownRatio = unknownCount / exports.length;
|
|
477
|
+
return Math.round((1.0 - unknownRatio * 0.5) * 100) / 100;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { ManifestGenerator } from './generator.js';
|
|
2
|
+
export { ManifestResolver } from './resolver.js';
|
|
3
|
+
export type { ResolveResult, ManifestSummary } from './resolver.js';
|
|
4
|
+
export type {
|
|
5
|
+
Manifest,
|
|
6
|
+
ManifestExport,
|
|
7
|
+
ManifestImport,
|
|
8
|
+
ManifestParam,
|
|
9
|
+
ManifestCapabilities,
|
|
10
|
+
ManifestPackage,
|
|
11
|
+
ManifestAccess,
|
|
12
|
+
ManifestLanguageSpecific,
|
|
13
|
+
EffectType,
|
|
14
|
+
FlowType,
|
|
15
|
+
ExportKind,
|
|
16
|
+
} from './types.js';
|