@grafema/util 0.3.18 → 0.3.20

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 (45) hide show
  1. package/dist/federation/FederatedRouter.d.ts +124 -0
  2. package/dist/federation/FederatedRouter.d.ts.map +1 -0
  3. package/dist/federation/FederatedRouter.js +297 -0
  4. package/dist/federation/FederatedRouter.js.map +1 -0
  5. package/dist/federation/ShardDiscovery.d.ts +56 -0
  6. package/dist/federation/ShardDiscovery.d.ts.map +1 -0
  7. package/dist/federation/ShardDiscovery.js +100 -0
  8. package/dist/federation/ShardDiscovery.js.map +1 -0
  9. package/dist/federation/index.d.ts +28 -0
  10. package/dist/federation/index.d.ts.map +1 -0
  11. package/dist/federation/index.js +26 -0
  12. package/dist/federation/index.js.map +1 -0
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/manifest/generator.d.ts.map +1 -1
  18. package/dist/manifest/generator.js +26 -4
  19. package/dist/manifest/generator.js.map +1 -1
  20. package/dist/manifest/index.d.ts +2 -0
  21. package/dist/manifest/index.d.ts.map +1 -1
  22. package/dist/manifest/index.js +1 -0
  23. package/dist/manifest/index.js.map +1 -1
  24. package/dist/manifest/registry.d.ts +116 -0
  25. package/dist/manifest/registry.d.ts.map +1 -0
  26. package/dist/manifest/registry.js +638 -0
  27. package/dist/manifest/registry.js.map +1 -0
  28. package/dist/manifest/resolver.d.ts +9 -0
  29. package/dist/manifest/resolver.d.ts.map +1 -1
  30. package/dist/manifest/resolver.js +31 -0
  31. package/dist/manifest/resolver.js.map +1 -1
  32. package/dist/notation/traceRenderer.d.ts +2 -0
  33. package/dist/notation/traceRenderer.d.ts.map +1 -1
  34. package/dist/notation/traceRenderer.js +6 -5
  35. package/dist/notation/traceRenderer.js.map +1 -1
  36. package/package.json +3 -3
  37. package/src/federation/FederatedRouter.ts +440 -0
  38. package/src/federation/ShardDiscovery.ts +130 -0
  39. package/src/federation/index.ts +35 -0
  40. package/src/index.ts +16 -1
  41. package/src/manifest/generator.ts +25 -4
  42. package/src/manifest/index.ts +2 -0
  43. package/src/manifest/registry.ts +769 -0
  44. package/src/manifest/resolver.ts +33 -0
  45. package/src/notation/traceRenderer.ts +8 -5
@@ -0,0 +1,130 @@
1
+ /**
2
+ * ShardDiscovery — discovers registered RFDB shards via /tmp/rfdb-shards/.
3
+ *
4
+ * Each rfdb-server running with --federate writes a JSON registration file
5
+ * containing its root path, socket, port, and PID. ShardDiscovery scans
6
+ * these files and resolves file paths to the appropriate shard.
7
+ *
8
+ * Discovery is by file path prefix: if a target file's absolute path
9
+ * starts with a shard's root, that shard owns the file.
10
+ */
11
+
12
+ import { readFileSync, readdirSync, existsSync } from 'fs';
13
+ import { join } from 'path';
14
+
15
+ export interface ShardRegistration {
16
+ /** Absolute path of the project root this shard covers */
17
+ root: string;
18
+ /** Unix socket path for this shard */
19
+ socket: string;
20
+ /** WebSocket port (if available) */
21
+ wsPort?: number;
22
+ /** Process ID of the rfdb-server */
23
+ pid: number;
24
+ /** Epoch timestamp when the server started */
25
+ started: number;
26
+ /** Server version */
27
+ serverVersion: string;
28
+ }
29
+
30
+ const SHARDS_DIR = '/tmp/rfdb-shards';
31
+
32
+ export class ShardDiscovery {
33
+ /** root path → registration (sorted by longest root first for prefix matching) */
34
+ private shards = new Map<string, ShardRegistration>();
35
+
36
+ /** Number of discovered shards */
37
+ get size(): number {
38
+ return this.shards.size;
39
+ }
40
+
41
+ /** All registered shard roots */
42
+ get roots(): string[] {
43
+ return [...this.shards.keys()];
44
+ }
45
+
46
+ /**
47
+ * Scan /tmp/rfdb-shards/ for registered shards.
48
+ * Validates each registration: checks PID is alive, parses JSON.
49
+ * Call this periodically or before federated queries.
50
+ */
51
+ scan(): number {
52
+ this.shards.clear();
53
+
54
+ if (!existsSync(SHARDS_DIR)) {
55
+ return 0;
56
+ }
57
+
58
+ const files = readdirSync(SHARDS_DIR).filter(f => f.endsWith('.json'));
59
+
60
+ for (const file of files) {
61
+ try {
62
+ const raw = readFileSync(join(SHARDS_DIR, file), 'utf-8');
63
+ const reg = JSON.parse(raw) as ShardRegistration;
64
+
65
+ if (!reg.root || !reg.socket || !reg.pid) continue;
66
+
67
+ // Check if process is still alive
68
+ if (!isProcessAlive(reg.pid)) continue;
69
+
70
+ // Normalize root path (ensure no trailing slash)
71
+ const root = reg.root.replace(/\/$/, '');
72
+ this.shards.set(root, { ...reg, root });
73
+ } catch {
74
+ // Skip malformed registration files
75
+ }
76
+ }
77
+
78
+ return this.shards.size;
79
+ }
80
+
81
+ /**
82
+ * Find the shard that owns a given absolute file path.
83
+ * Uses longest-prefix matching: /repo/packages/api/ wins over /repo/.
84
+ */
85
+ resolve(absoluteFilePath: string): ShardRegistration | null {
86
+ let bestMatch: ShardRegistration | null = null;
87
+ let bestLength = 0;
88
+
89
+ for (const [root, reg] of this.shards) {
90
+ if (absoluteFilePath.startsWith(root) && root.length > bestLength) {
91
+ bestMatch = reg;
92
+ bestLength = root.length;
93
+ }
94
+ }
95
+
96
+ return bestMatch;
97
+ }
98
+
99
+ /**
100
+ * Get all known shards (for scatter-gather queries).
101
+ */
102
+ all(): ShardRegistration[] {
103
+ return [...this.shards.values()];
104
+ }
105
+
106
+ /**
107
+ * Register a shard programmatically (for testing or remote shards).
108
+ */
109
+ register(reg: ShardRegistration): void {
110
+ const root = reg.root.replace(/\/$/, '');
111
+ this.shards.set(root, { ...reg, root });
112
+ }
113
+
114
+ /**
115
+ * Remove a shard by root path.
116
+ */
117
+ unregister(root: string): void {
118
+ this.shards.delete(root.replace(/\/$/, ''));
119
+ }
120
+ }
121
+
122
+ /** Check if a process with given PID is alive */
123
+ function isProcessAlive(pid: number): boolean {
124
+ try {
125
+ process.kill(pid, 0);
126
+ return true;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Federation module — cross-shard query routing for RFDB.
3
+ *
4
+ * Enables multiple RFDB shards to discover each other and resolve
5
+ * cross-shard references at query time. Within a shard, the graph
6
+ * is fully materialized (analysis-time). Between shards, references
7
+ * are resolved lazily (execution-time) via the SUBGRAPH primitive.
8
+ *
9
+ * ## Architecture
10
+ *
11
+ * - ShardDiscovery: scans /tmp/rfdb-shards/ for registered shards
12
+ * - FederatedRouter: orchestrates cross-shard traversal and scatter-gather
13
+ * - Each shard is a separate rfdb-server process with its own graph
14
+ * - Discovery is by file path prefix matching (zero-config)
15
+ *
16
+ * ## Usage
17
+ *
18
+ * const discovery = new ShardDiscovery();
19
+ * await discovery.scan();
20
+ *
21
+ * const router = new FederatedRouter(discovery, localClient);
22
+ * const result = await router.traceDataflow('src/app.ts->userInput', 'forward', 10);
23
+ */
24
+
25
+ export { ShardDiscovery } from './ShardDiscovery.js';
26
+ export type { ShardRegistration } from './ShardDiscovery.js';
27
+
28
+ export { FederatedRouter } from './FederatedRouter.js';
29
+ export type {
30
+ FederatedTraceResult,
31
+ FederatedTraceHop,
32
+ FrontierEdge,
33
+ SubgraphResponse,
34
+ ManifestResolvedNode,
35
+ } from './FederatedRouter.js';
package/src/index.ts CHANGED
@@ -257,8 +257,19 @@ export type {
257
257
  TraceNarrativeOptions,
258
258
  } from './notation/index.js';
259
259
 
260
+ // Federation — cross-shard query routing
261
+ export { ShardDiscovery, FederatedRouter } from './federation/index.js';
262
+ export type {
263
+ ShardRegistration,
264
+ FederatedTraceResult,
265
+ FederatedTraceHop,
266
+ FrontierEdge,
267
+ SubgraphResponse,
268
+ ManifestResolvedNode,
269
+ } from './federation/index.js';
270
+
260
271
  // Manifest generation & resolution (federation)
261
- export { ManifestGenerator, ManifestResolver } from './manifest/index.js';
272
+ export { ManifestGenerator, ManifestResolver, RegistryBuilder, resolvePackageDir, detectSourceType, resolveEntryPoint } from './manifest/index.js';
262
273
  export type {
263
274
  Manifest,
264
275
  ManifestExport,
@@ -269,6 +280,10 @@ export type {
269
280
  ExportKind,
270
281
  ResolveResult,
271
282
  ManifestSummary,
283
+ RegistryEntry,
284
+ RegistryIndex,
285
+ RegistryBuilderOptions,
286
+ BuildResult,
272
287
  } from './manifest/index.js';
273
288
 
274
289
  // Re-export types for convenience
@@ -174,9 +174,13 @@ export class ManifestGenerator {
174
174
  // Graph-based approach:
175
175
  // EXPORT(named) --EXPORTS--> EXPORT_BINDING(name, source) --> definition in source file
176
176
  await this.collectExportsViaBindings(entryFile, exports, seen, new Set());
177
- } else {
178
- // Fallback: all exported definitions from the package
177
+ }
178
+
179
+ // Fallback: if entry-file mode found 0 exports (e.g., CJS barrel,
180
+ // broken re-export chain in compiled_js), scan all exported definitions
181
+ if (exports.length === 0) {
179
182
  const prefix = this.options.packagePrefix ?? '';
183
+ // Check standard definition types (FUNCTION, CLASS, CONSTANT, INTERFACE)
180
184
  for (const type of ManifestGenerator.DEF_TYPES) {
181
185
  for await (const node of this.backend.queryNodes({ type: type as never })) {
182
186
  if (prefix && !node.file?.startsWith(prefix)) continue;
@@ -187,6 +191,21 @@ export class ManifestGenerator {
187
191
  await this.addExportFromDefinition(node, exports);
188
192
  }
189
193
  }
194
+ // Also check EXPORT_BINDING nodes (from CJS exports.foo = ...)
195
+ if (exports.length === 0) {
196
+ for await (const node of this.backend.queryNodes({ type: 'EXPORT_BINDING' as never })) {
197
+ if (prefix && !node.file?.startsWith(prefix)) continue;
198
+ if (!node.name || node.name === 'named' || node.name === 'default') continue;
199
+ if (seen.has(node.name)) continue;
200
+ seen.add(node.name);
201
+ exports.push({
202
+ name: node.name,
203
+ kind: 'VARIABLE',
204
+ semanticId: node.id,
205
+ effects: ['UNKNOWN'],
206
+ });
207
+ }
208
+ }
190
209
  }
191
210
 
192
211
  exports.sort((a, b) => a.name.localeCompare(b.name));
@@ -276,7 +295,8 @@ export class ManifestGenerator {
276
295
  const dir = fromFile.substring(0, fromFile.lastIndexOf('/'));
277
296
  let resolved = source.startsWith('.') ? `${dir}/${source}` : source;
278
297
  resolved = resolved.replace(/\/\.\//g, '/');
279
- if (resolved.endsWith('.js')) {
298
+ // Only replace .js → .ts for source TypeScript code, not compiled_js
299
+ if (this.options.sourceType !== 'compiled_js' && resolved.endsWith('.js')) {
280
300
  resolved = resolved.replace(/\.js$/, '.ts');
281
301
  }
282
302
  return resolved;
@@ -330,7 +350,8 @@ export class ManifestGenerator {
330
350
  /** Enrich all exports with computed effects (transitive call graph analysis) */
331
351
  private async enrichEffects(exports: ManifestExport[]): Promise<void> {
332
352
  for (const entry of exports) {
333
- if (entry.kind !== 'FUNCTION' && entry.kind !== 'CLASS') continue;
353
+ // Enrich FUNCTION, CLASS, and VARIABLE (CJS exports may be VARIABLE kind)
354
+ if (entry.kind !== 'FUNCTION' && entry.kind !== 'CLASS' && entry.kind !== 'VARIABLE') continue;
334
355
 
335
356
  const effects = new Set<EffectType>();
336
357
  const visited = new Set<string>();
@@ -1,6 +1,8 @@
1
1
  export { ManifestGenerator } from './generator.js';
2
2
  export { ManifestResolver } from './resolver.js';
3
3
  export type { ResolveResult, ManifestSummary } from './resolver.js';
4
+ export { RegistryBuilder, resolvePackageDir, detectSourceType, resolveEntryPoint } from './registry.js';
5
+ export type { RegistryEntry, RegistryIndex, RegistryBuilderOptions, BuildResult } from './registry.js';
4
6
  export type {
5
7
  Manifest,
6
8
  ManifestExport,