@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.
@@ -0,0 +1,248 @@
1
+ /**
2
+ * ManifestResolver — resolves cross-package imports via manifest.yaml files.
3
+ *
4
+ * Given an import like `import { GraphBackend } from '@grafema/util'`,
5
+ * the resolver loads @grafema/util's manifest.yaml and returns the
6
+ * ManifestExport entry for GraphBackend (kind, effects, semanticId).
7
+ *
8
+ * This is the read-side complement to ManifestGenerator (write-side).
9
+ *
10
+ * Usage:
11
+ * const resolver = new ManifestResolver();
12
+ * resolver.loadFromWorkspace('/project', ['packages/util', 'packages/types']);
13
+ * resolver.loadFromNodeModules('/project/node_modules');
14
+ *
15
+ * const result = resolver.resolve('@grafema/util', 'GraphBackend');
16
+ * // → { name: 'GraphBackend', kind: 'CLASS', effects: ['PURE'], semanticId: '...' }
17
+ */
18
+
19
+ import { readFileSync, existsSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { parse as parseYaml } from 'yaml';
22
+ import type { Manifest, ManifestExport } from './types.js';
23
+
24
+ export interface ResolveResult {
25
+ /** The matched export entry */
26
+ export: ManifestExport;
27
+ /** Package purl this export belongs to */
28
+ packagePurl: string;
29
+ /** Confidence of the source manifest */
30
+ confidence: number;
31
+ /** Whether the manifest has a full graph available */
32
+ hasGraph: boolean;
33
+ }
34
+
35
+ export class ManifestResolver {
36
+ /** package name → loaded Manifest */
37
+ private manifests = new Map<string, Manifest>();
38
+ /** package name → Map<export name → ManifestExport> (index for O(1) lookup) */
39
+ private exportIndex = new Map<string, Map<string, ManifestExport>>();
40
+
41
+ /** Number of loaded manifests */
42
+ get size(): number {
43
+ return this.manifests.size;
44
+ }
45
+
46
+ /** All loaded package names */
47
+ get packages(): string[] {
48
+ return [...this.manifests.keys()];
49
+ }
50
+
51
+ /**
52
+ * Load a single manifest from a YAML file path.
53
+ * The package name is extracted from the manifest's purl field.
54
+ */
55
+ loadFromFile(filePath: string): Manifest | null {
56
+ if (!existsSync(filePath)) return null;
57
+
58
+ try {
59
+ const raw = readFileSync(filePath, 'utf-8');
60
+ const manifest = parseYaml(raw) as Manifest;
61
+ if (!manifest?.package?.purl || !manifest.exports) return null;
62
+
63
+ const pkgName = purlToPackageName(manifest.package.purl);
64
+ this.register(pkgName, manifest);
65
+ return manifest;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Load manifests from workspace packages.
73
+ * Scans each directory for manifest.yaml.
74
+ *
75
+ * @param rootDir Project root directory
76
+ * @param packageDirs Relative paths to package directories (e.g., ['packages/util', 'packages/types'])
77
+ */
78
+ loadFromWorkspace(rootDir: string, packageDirs: string[]): number {
79
+ let loaded = 0;
80
+ for (const dir of packageDirs) {
81
+ const manifestPath = join(rootDir, dir, 'manifest.yaml');
82
+ if (this.loadFromFile(manifestPath)) loaded++;
83
+ }
84
+ return loaded;
85
+ }
86
+
87
+ /**
88
+ * Auto-discover and load manifests from node_modules.
89
+ * Scans node_modules for packages containing manifest.yaml.
90
+ * Optionally scoped to specific package names.
91
+ *
92
+ * @param nodeModulesDir Path to node_modules directory
93
+ * @param packageNames Optional list of package names to load (e.g., ['@grafema/util']). If omitted, scans all.
94
+ */
95
+ loadFromNodeModules(nodeModulesDir: string, packageNames?: string[]): number {
96
+ if (!existsSync(nodeModulesDir)) return 0;
97
+
98
+ let loaded = 0;
99
+ if (packageNames) {
100
+ for (const name of packageNames) {
101
+ const manifestPath = join(nodeModulesDir, name, 'manifest.yaml');
102
+ if (this.loadFromFile(manifestPath)) loaded++;
103
+ }
104
+ }
105
+ return loaded;
106
+ }
107
+
108
+ /**
109
+ * Register a manifest directly (e.g., from in-memory generation).
110
+ */
111
+ register(packageName: string, manifest: Manifest): void {
112
+ this.manifests.set(packageName, manifest);
113
+
114
+ // Build export index
115
+ const index = new Map<string, ManifestExport>();
116
+ for (const exp of manifest.exports) {
117
+ index.set(exp.name, exp);
118
+ }
119
+ this.exportIndex.set(packageName, index);
120
+ }
121
+
122
+ /**
123
+ * Resolve an import: given package name and symbol, return the export info.
124
+ *
125
+ * @param source Import source (e.g., '@grafema/util', 'graphql-yoga')
126
+ * @param symbolName Exported symbol name (e.g., 'GraphBackend')
127
+ * @returns ResolveResult or null if not found
128
+ */
129
+ resolve(source: string, symbolName: string): ResolveResult | null {
130
+ const index = this.exportIndex.get(source);
131
+ if (!index) return null;
132
+
133
+ const exp = index.get(symbolName);
134
+ if (!exp) return null;
135
+
136
+ const manifest = this.manifests.get(source)!;
137
+ return {
138
+ export: exp,
139
+ packagePurl: manifest.package.purl,
140
+ confidence: manifest.confidence,
141
+ hasGraph: manifest.capabilities.has_graph,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Resolve all symbols from a specific import source.
147
+ * Returns a map of symbol name → ManifestExport for all known exports.
148
+ *
149
+ * Useful for: `import * as utils from '@grafema/util'`
150
+ */
151
+ resolveAll(source: string): Map<string, ManifestExport> | null {
152
+ return this.exportIndex.get(source) ?? null;
153
+ }
154
+
155
+ /**
156
+ * Get the full manifest for a package.
157
+ */
158
+ getManifest(packageName: string): Manifest | null {
159
+ return this.manifests.get(packageName) ?? null;
160
+ }
161
+
162
+ /**
163
+ * Check if a package has a loaded manifest.
164
+ */
165
+ has(packageName: string): boolean {
166
+ return this.manifests.has(packageName);
167
+ }
168
+
169
+ /**
170
+ * Get the effects for an imported symbol.
171
+ * Shorthand for resolve() when you only need effects.
172
+ */
173
+ getEffects(source: string, symbolName: string): string[] | null {
174
+ return this.resolve(source, symbolName)?.export.effects ?? null;
175
+ }
176
+
177
+ /**
178
+ * Build a dependency graph from loaded manifests.
179
+ * Returns: Map<packageName → Set<dependencyPackageName>>
180
+ */
181
+ buildDependencyGraph(): Map<string, Set<string>> {
182
+ const deps = new Map<string, Set<string>>();
183
+
184
+ for (const [name, manifest] of this.manifests) {
185
+ const pkgDeps = new Set<string>();
186
+ for (const imp of manifest.imports) {
187
+ const depName = purlToPackageName(imp.purl);
188
+ if (depName && this.manifests.has(depName)) {
189
+ pkgDeps.add(depName);
190
+ }
191
+ }
192
+ deps.set(name, pkgDeps);
193
+ }
194
+
195
+ return deps;
196
+ }
197
+
198
+ /**
199
+ * Get summary stats for all loaded manifests.
200
+ */
201
+ getSummary(): ManifestSummary[] {
202
+ const summaries: ManifestSummary[] = [];
203
+ for (const [name, manifest] of this.manifests) {
204
+ summaries.push({
205
+ packageName: name,
206
+ purl: manifest.package.purl,
207
+ totalExports: manifest.exports.length,
208
+ totalImports: manifest.imports.length,
209
+ confidence: manifest.confidence,
210
+ hasGraph: manifest.capabilities.has_graph,
211
+ exportsByKind: countByKind(manifest.exports),
212
+ });
213
+ }
214
+ return summaries.sort((a, b) => a.packageName.localeCompare(b.packageName));
215
+ }
216
+ }
217
+
218
+ export interface ManifestSummary {
219
+ packageName: string;
220
+ purl: string;
221
+ totalExports: number;
222
+ totalImports: number;
223
+ confidence: number;
224
+ hasGraph: boolean;
225
+ exportsByKind: Record<string, number>;
226
+ }
227
+
228
+ // ── Helpers ──────────────────────────────────────────────
229
+
230
+ /** Extract npm package name from a purl like "pkg:npm/@grafema/util@0.2.0" */
231
+ function purlToPackageName(purl: string): string {
232
+ // pkg:npm/@scope/name@version or pkg:npm/name@version
233
+ const match = purl.match(/^pkg:npm\/(.+?)(?:@[\d.]+.*)?$/);
234
+ if (!match) return purl;
235
+
236
+ const name = match[1];
237
+ // For node builtins like "pkg:npm/node@*#fs", skip
238
+ if (name === 'node') return '';
239
+ return name;
240
+ }
241
+
242
+ function countByKind(exports: ManifestExport[]): Record<string, number> {
243
+ const counts: Record<string, number> = {};
244
+ for (const e of exports) {
245
+ counts[e.kind] = (counts[e.kind] || 0) + 1;
246
+ }
247
+ return counts;
248
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Manifest format types for Grafema federation.
3
+ *
4
+ * A manifest describes a package's export surface: what it exports,
5
+ * what effects each export has, and what it imports.
6
+ *
7
+ * Manifest is the boundary contract between analyzed packages.
8
+ * It's polyglot (purl URIs), versioned (schema + analyzer),
9
+ * and layered (CDN < auto-generated < developer-authored).
10
+ */
11
+
12
+ // === Effect taxonomy (from effects-db/taxonomy.yaml) ===
13
+
14
+ export type EffectType =
15
+ | 'PURE'
16
+ | 'MUTATION'
17
+ | 'IO'
18
+ | 'THROW'
19
+ | 'ASYNC'
20
+ | 'NONDETERMINISTIC'
21
+ | 'UNKNOWN';
22
+
23
+ // === Flow types (universal across languages) ===
24
+
25
+ export type FlowType =
26
+ | 'IN' // read only
27
+ | 'OUT' // return value (new data)
28
+ | 'PIPE' // read and modified (pass-through mutation)
29
+ | 'SINK'; // consumed, not returned
30
+
31
+ // === Export kinds ===
32
+
33
+ export type ExportKind =
34
+ | 'FUNCTION'
35
+ | 'CLASS'
36
+ | 'VARIABLE'
37
+ | 'TYPE'
38
+ | 'CONSTANT'
39
+ | 'INTERFACE';
40
+
41
+ // === Manifest schema ===
42
+
43
+ export interface ManifestParam {
44
+ name: string;
45
+ flow: FlowType;
46
+ }
47
+
48
+ export interface ManifestExport {
49
+ name: string;
50
+ kind: ExportKind;
51
+ semanticId: string;
52
+ effects: EffectType[];
53
+ params?: ManifestParam[];
54
+ returns?: { flow: FlowType };
55
+ fields?: string[]; // for TYPE/INTERFACE exports
56
+ }
57
+
58
+ export interface ManifestImport {
59
+ purl: string;
60
+ symbols: string[];
61
+ }
62
+
63
+ export interface ManifestCapabilities {
64
+ total_exports: number;
65
+ total_internal_symbols: number;
66
+ has_graph: boolean;
67
+ }
68
+
69
+ export interface ManifestPackage {
70
+ purl: string;
71
+ checksum?: string;
72
+ source_type: 'source' | 'compiled_js' | 'minified' | 'dts_only';
73
+ }
74
+
75
+ export interface ManifestAccess {
76
+ local?: string;
77
+ remote?: string;
78
+ }
79
+
80
+ export interface ManifestLanguageSpecific {
81
+ module_system: 'esm' | 'cjs' | 'dual';
82
+ entry_points?: Record<string, string>;
83
+ typescript_declarations?: boolean;
84
+ }
85
+
86
+ export interface Manifest {
87
+ schema_version: number;
88
+ analyzer_version: string;
89
+ authored: boolean;
90
+ confidence: number;
91
+ generated: string; // ISO 8601
92
+
93
+ package: ManifestPackage;
94
+ exports: ManifestExport[];
95
+ imports: ManifestImport[];
96
+ capabilities: ManifestCapabilities;
97
+ access?: ManifestAccess;
98
+
99
+ language?: string;
100
+ language_specific?: ManifestLanguageSpecific;
101
+ }
@@ -142,6 +142,7 @@ export const EDGE_ARCHETYPE_MAP: Record<string, EdgeMapping> = {
142
142
  AFFECTS: m('governs', '|=', 'affects'),
143
143
  MONITORED_BY: m('governs', '|=', 'monitored by'),
144
144
  MEASURED_BY: m('governs', '|=', 'measured by'),
145
+ OBSERVES: m('governs', '|=', 'observes'),
145
146
  PROVISIONED_BY: m('governs', '|=', 'provisioned by'),
146
147
  REGISTERS_VIEW: m('governs', '|=', 'registers view'),
147
148