@shapeshift-labs/loom 0.1.15 → 0.1.17

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 (59) hide show
  1. package/README.md +156 -6
  2. package/dist/capabilities.d.ts.map +1 -1
  3. package/dist/capabilities.js +21 -3
  4. package/dist/capabilities.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +160 -2
  7. package/dist/cli.js.map +1 -1
  8. package/dist/delegate.d.ts +9 -4
  9. package/dist/delegate.d.ts.map +1 -1
  10. package/dist/delegate.js +113 -3
  11. package/dist/delegate.js.map +1 -1
  12. package/dist/graph-frontier-run-convert.d.ts +6 -0
  13. package/dist/graph-frontier-run-convert.d.ts.map +1 -0
  14. package/dist/graph-frontier-run-convert.js +219 -0
  15. package/dist/graph-frontier-run-convert.js.map +1 -0
  16. package/dist/graph-frontier-run-records.d.ts +9 -0
  17. package/dist/graph-frontier-run-records.d.ts.map +1 -0
  18. package/dist/graph-frontier-run-records.js +105 -0
  19. package/dist/graph-frontier-run-records.js.map +1 -0
  20. package/dist/graph-frontier-run.d.ts +6 -0
  21. package/dist/graph-frontier-run.d.ts.map +1 -0
  22. package/dist/graph-frontier-run.js +63 -0
  23. package/dist/graph-frontier-run.js.map +1 -0
  24. package/dist/graph-live.d.ts +6 -0
  25. package/dist/graph-live.d.ts.map +1 -0
  26. package/dist/graph-live.js +251 -0
  27. package/dist/graph-live.js.map +1 -0
  28. package/dist/graph.d.ts +42 -1
  29. package/dist/graph.d.ts.map +1 -1
  30. package/dist/graph.js +824 -1
  31. package/dist/graph.js.map +1 -1
  32. package/dist/help.d.ts.map +1 -1
  33. package/dist/help.js +27 -0
  34. package/dist/help.js.map +1 -1
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/output.d.ts.map +1 -1
  40. package/dist/output.js +45 -1
  41. package/dist/output.js.map +1 -1
  42. package/dist/status-ui-health.d.ts +4 -0
  43. package/dist/status-ui-health.d.ts.map +1 -0
  44. package/dist/status-ui-health.js +91 -0
  45. package/dist/status-ui-health.js.map +1 -0
  46. package/dist/status-ui.d.ts +40 -0
  47. package/dist/status-ui.d.ts.map +1 -0
  48. package/dist/status-ui.js +249 -0
  49. package/dist/status-ui.js.map +1 -0
  50. package/dist/status.d.ts.map +1 -1
  51. package/dist/status.js +5 -1
  52. package/dist/status.js.map +1 -1
  53. package/dist/swarm.d.ts.map +1 -1
  54. package/dist/swarm.js +2 -0
  55. package/dist/swarm.js.map +1 -1
  56. package/dist/types.d.ts +409 -0
  57. package/dist/types.d.ts.map +1 -1
  58. package/frontier.source-policy.json +21 -1
  59. package/package.json +5 -3
package/dist/graph.js CHANGED
@@ -1,5 +1,84 @@
1
- import { abs, pathExists, readJson, resolveRoot } from './common.js';
1
+ import path from 'node:path';
2
+ import { abs, nowIso, pathExists, readJson, resolveRoot, writeJson } from './common.js';
2
3
  import { readLoomConfig } from './config.js';
4
+ import { FRONTIER_SWARM_CODEX_LIVE_RUN_GRAPH_EVENTS_ARTIFACT, materializeSwarmCodexLiveRunGraphEvents } from './graph-live.js';
5
+ import { FRONTIER_RUN_GRAPH_SOURCE, normalizeFrontierRunEvents, parseFrontierRunEventsInput } from './graph-frontier-run.js';
6
+ export const LOOM_NATIVE_RUN_GRAPH_SOURCE = 'loom-native';
7
+ export const FRONTIER_SWARM_CODEX_RUN_GRAPH_SOURCE = 'frontier-swarm-codex';
8
+ export const LOOM_RUN_GRAPH_CHUNK_TEMPLATE_KIND = 'loom.run-graph.chunk-template';
9
+ export { FRONTIER_SWARM_CODEX_LIVE_RUN_GRAPH_EVENTS_ARTIFACT, FRONTIER_SWARM_CODEX_LIVE_RUN_GRAPH_EVENT_KIND, materializeSwarmCodexLiveRunGraphEvents, parseSwarmCodexRunGraphInput } from './graph-live.js';
10
+ export { FRONTIER_RUN_GRAPH_SOURCE, normalizeFrontierRunEvents, parseFrontierRunEventsInput } from './graph-frontier-run.js';
11
+ export function buildRunGraphChunkTemplate(options) {
12
+ const nodes = uniqueStrings([
13
+ ...options.nodes,
14
+ ...options.edges.flatMap((edge) => [edge.from, edge.to]),
15
+ ...options.entryNodes,
16
+ ...options.exitNodes,
17
+ ...Object.values(options.roles ?? {}).flatMap((values) => [...values])
18
+ ]);
19
+ const edges = uniqueEdges(options.edges);
20
+ return {
21
+ kind: LOOM_RUN_GRAPH_CHUNK_TEMPLATE_KIND,
22
+ version: 1,
23
+ id: options.id ?? stableChunkId(options.chunkKind, nodes, edges),
24
+ chunkKind: options.chunkKind,
25
+ nodes,
26
+ edges,
27
+ entryNodes: uniqueStrings(options.entryNodes),
28
+ exitNodes: uniqueStrings(options.exitNodes),
29
+ roles: normalizeChunkRoles(options.roles ?? {}),
30
+ ...(options.metadata ? { metadata: options.metadata } : {})
31
+ };
32
+ }
33
+ export function buildRunGraphChainChunk(nodes, options = {}) {
34
+ const list = requiredNodes('chain nodes', nodes);
35
+ return buildRunGraphChunkTemplate({
36
+ id: options.id,
37
+ chunkKind: 'chain',
38
+ nodes: list,
39
+ edges: pairwiseEdges(list, options.edgeType ?? 'depends-on'),
40
+ entryNodes: [list[0]],
41
+ exitNodes: [list[list.length - 1]],
42
+ roles: { chain: list }
43
+ });
44
+ }
45
+ export function buildRunGraphForkChunk(source, branches, options = {}) {
46
+ const targets = requiredNodes('fork branches', branches);
47
+ return buildRunGraphChunkTemplate({
48
+ id: options.id,
49
+ chunkKind: 'fork',
50
+ nodes: [source, ...targets],
51
+ edges: targets.map((target) => ({ from: source, to: target, type: options.edgeType ?? 'depends-on' })),
52
+ entryNodes: [source],
53
+ exitNodes: targets,
54
+ roles: { source: [source], branches: targets }
55
+ });
56
+ }
57
+ export function buildRunGraphJoinChunk(branches, target, options = {}) {
58
+ const inputs = requiredNodes('join branches', branches);
59
+ return buildRunGraphChunkTemplate({
60
+ id: options.id,
61
+ chunkKind: 'join',
62
+ nodes: [...inputs, target],
63
+ edges: inputs.map((input) => ({ from: input, to: target, type: options.edgeType ?? 'depends-on' })),
64
+ entryNodes: inputs,
65
+ exitNodes: [target],
66
+ roles: { branches: inputs, target: [target] }
67
+ });
68
+ }
69
+ export function buildRunGraphPatternChunk(chunkKind, nodes, options = {}) {
70
+ const list = requiredNodes(`${chunkKind} nodes`, nodes);
71
+ return buildRunGraphChunkTemplate({
72
+ id: options.id,
73
+ chunkKind,
74
+ nodes: list,
75
+ edges: pairwiseEdges(list, options.edgeType ?? 'depends-on'),
76
+ entryNodes: [list[0]],
77
+ exitNodes: [list[list.length - 1]],
78
+ roles: { [chunkKind]: list },
79
+ metadata: options.metadata
80
+ });
81
+ }
3
82
  export async function readLoomGraph(options = {}) {
4
83
  const root = resolveRoot(options.root);
5
84
  const config = await readLoomConfig(root);
@@ -8,4 +87,748 @@ export async function readLoomGraph(options = {}) {
8
87
  throw new Error('missing .loom graph; run loom scan first');
9
88
  return readJson(file);
10
89
  }
90
+ export async function readLoomRunGraph(options = {}) {
91
+ const root = resolveRoot(options.root);
92
+ const config = await readLoomConfig(root);
93
+ const file = loomRunGraphFile(root, config, options.runId);
94
+ if (!(await pathExists(file)))
95
+ throw new Error('missing .loom run graph; write one with writeLoomRunGraph first');
96
+ const graph = await readJson(file);
97
+ assertLoomRunGraph(graph, file);
98
+ return graph;
99
+ }
100
+ export async function writeLoomRunGraph(graph, options = {}) {
101
+ const root = resolveRoot(options.root);
102
+ const config = await readLoomConfig(root);
103
+ assertLoomRunGraph(graph);
104
+ const file = loomRunGraphFile(root, config, options.runId ?? graph.runId);
105
+ await writeJson(file, graph);
106
+ return file;
107
+ }
108
+ export function normalizeSwarmCodexRunGraph(input, options = {}) {
109
+ assertSwarmCodexRunGraph(input);
110
+ return normalizeSwarmCodexRunGraphArtifact(input, options);
111
+ }
112
+ export function normalizeSwarmCodexLiveRunGraphEvents(input, options = {}) {
113
+ const artifact = materializeSwarmCodexLiveRunGraphEvents(input, options);
114
+ const eventTypes = Array.from(new Set(input.map((event) => event.type).filter(Boolean)));
115
+ return normalizeSwarmCodexRunGraphArtifact(artifact, options, {
116
+ artifactKind: FRONTIER_SWARM_CODEX_LIVE_RUN_GRAPH_EVENTS_ARTIFACT,
117
+ eventCount: input.length,
118
+ eventTypes
119
+ }, {
120
+ swarmCodexLive: toJsonValue({
121
+ eventCount: input.length,
122
+ eventTypes,
123
+ firstGeneratedAt: input[0]?.generatedAt,
124
+ lastGeneratedAt: input.at(-1)?.generatedAt
125
+ })
126
+ }, input);
127
+ }
128
+ function normalizeSwarmCodexRunGraphArtifact(input, options = {}, sourceMetadata = {}, extraMetadata = {}, liveEvents = []) {
129
+ const root = resolveRoot(options.root);
130
+ const runId = options.runId ?? swarmRunGraphRunId(input);
131
+ const graph = normalizeSwarmCodexJobGraph(input);
132
+ const decisionGraph = normalizeSwarmCodexDecisionGraph(input, liveEvents);
133
+ const typedCounts = summarizeTypedCounts(decisionGraph);
134
+ const panels = createLoomRunGraphPanelRecords(decisionGraph);
135
+ return {
136
+ kind: 'loom.run-graph',
137
+ version: 1,
138
+ generatedAt: timestampToIso(input.generatedAt),
139
+ root,
140
+ runId,
141
+ source: FRONTIER_SWARM_CODEX_RUN_GRAPH_SOURCE,
142
+ sourceKind: FRONTIER_SWARM_CODEX_RUN_GRAPH_SOURCE,
143
+ sourceMetadata: {
144
+ kind: FRONTIER_SWARM_CODEX_RUN_GRAPH_SOURCE,
145
+ artifactKind: input.kind,
146
+ artifactId: input.id,
147
+ ...(options.sourcePath ? { path: options.sourcePath } : {}),
148
+ runDir: input.runDir,
149
+ outDir: input.outDir,
150
+ importedAt: nowIso(),
151
+ ...sourceMetadata
152
+ },
153
+ summary: {
154
+ nodes: graph.nodes.length,
155
+ edges: graph.edges.length,
156
+ roots: graph.roots.length,
157
+ leaves: graph.leaves.length,
158
+ issues: graph.issues.length,
159
+ ...(Object.keys(typedCounts).length ? { typedCounts } : {})
160
+ },
161
+ graph,
162
+ decisionGraph,
163
+ ...(panels.length ? { projections: { panels } } : {}),
164
+ metadata: {
165
+ swarmCodex: toJsonValue({
166
+ id: input.id,
167
+ summary: input.summary,
168
+ indexes: input.indexes,
169
+ nodes: input.nodes,
170
+ edges: input.edges
171
+ }),
172
+ ...extraMetadata
173
+ }
174
+ };
175
+ }
176
+ export async function importSwarmCodexRunGraph(input, options = {}) {
177
+ const graph = Array.isArray(input)
178
+ ? normalizeSwarmCodexLiveRunGraphEvents(input, options)
179
+ : normalizeSwarmCodexRunGraph(input, options);
180
+ const runId = options.runId ?? graph.runId ?? 'current';
181
+ const file = await writeLoomRunGraph(graph, { root: options.root, runId });
182
+ const artifactKind = graph.sourceMetadata?.artifactKind === FRONTIER_SWARM_CODEX_LIVE_RUN_GRAPH_EVENTS_ARTIFACT
183
+ ? 'live run graph events'
184
+ : 'run graph';
185
+ return {
186
+ ok: true,
187
+ message: `imported frontier-swarm-codex ${artifactKind} ${runId}`,
188
+ path: file,
189
+ runId,
190
+ present: true,
191
+ source: FRONTIER_SWARM_CODEX_RUN_GRAPH_SOURCE,
192
+ sourceKind: FRONTIER_SWARM_CODEX_RUN_GRAPH_SOURCE,
193
+ sourceMetadata: graph.sourceMetadata,
194
+ graphSummary: graph.summary
195
+ };
196
+ }
197
+ export async function importFrontierRunEvents(input, options = {}) {
198
+ const events = typeof input === 'string' ? parseFrontierRunEventsInput(input) : input;
199
+ const graph = normalizeFrontierRunEvents(events, options);
200
+ const runId = options.runId ?? graph.runId ?? 'current';
201
+ const file = await writeLoomRunGraph(graph, { root: options.root, runId });
202
+ return {
203
+ ok: true,
204
+ message: `imported frontier-run events ${runId}`,
205
+ path: file,
206
+ runId,
207
+ present: true,
208
+ source: FRONTIER_RUN_GRAPH_SOURCE,
209
+ sourceKind: FRONTIER_RUN_GRAPH_SOURCE,
210
+ sourceMetadata: graph.sourceMetadata,
211
+ graphSummary: graph.summary
212
+ };
213
+ }
214
+ export function loomRunGraphSourceKind(graph) {
215
+ return graph.sourceKind ?? graph.sourceMetadata?.kind ?? graph.source ?? LOOM_NATIVE_RUN_GRAPH_SOURCE;
216
+ }
217
+ function loomRunGraphFile(root, config, runId) {
218
+ return abs(root, `${config.generated.graph}/runs/${loomRunGraphFileName(runId)}.json`);
219
+ }
220
+ function loomRunGraphFileName(runId = 'current') {
221
+ const cleaned = runId.trim().replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
222
+ return cleaned || 'current';
223
+ }
224
+ function assertLoomRunGraph(value, file = 'run graph') {
225
+ if (!value || typeof value !== 'object')
226
+ throw new Error(`invalid ${file}: expected object`);
227
+ if (value.kind !== 'loom.run-graph')
228
+ throw new Error(`invalid ${file}: expected kind "loom.run-graph"`);
229
+ if (value.version !== 1)
230
+ throw new Error(`invalid ${file}: expected version 1`);
231
+ if (value.decisionGraph !== undefined)
232
+ assertLoomDecisionGraph(value.decisionGraph, file);
233
+ }
234
+ function normalizeSwarmCodexJobGraph(input) {
235
+ const nodeIds = new Set();
236
+ const edges = [];
237
+ const issues = [];
238
+ const seenEdges = new Set();
239
+ for (const node of input.nodes) {
240
+ if (typeof node.id === 'string' && node.id)
241
+ nodeIds.add(node.id);
242
+ }
243
+ for (const edge of input.edges) {
244
+ const from = typeof edge.from === 'string' ? edge.from : '';
245
+ const to = typeof edge.to === 'string' ? edge.to : '';
246
+ if (!from || !to) {
247
+ issues.push({
248
+ code: 'invalid-edge',
249
+ severity: 'warning',
250
+ message: `swarm edge ${edge.id || '<unknown>'} is missing from/to`
251
+ });
252
+ continue;
253
+ }
254
+ if (!nodeIds.has(from)) {
255
+ issues.push({
256
+ code: 'missing-edge-node',
257
+ severity: 'warning',
258
+ message: `swarm edge ${edge.id || '<unknown>'} references missing source node ${from}`,
259
+ path: `/edges/${edge.id || edges.length}/from`
260
+ });
261
+ nodeIds.add(from);
262
+ }
263
+ if (!nodeIds.has(to)) {
264
+ issues.push({
265
+ code: 'missing-edge-node',
266
+ severity: 'warning',
267
+ message: `swarm edge ${edge.id || '<unknown>'} references missing target node ${to}`,
268
+ path: `/edges/${edge.id || edges.length}/to`
269
+ });
270
+ nodeIds.add(to);
271
+ }
272
+ const normalized = { from, to, type: normalizeSwarmCodexEdgeKind(edge.kind) };
273
+ const key = `${normalized.type}\0${normalized.from}\0${normalized.to}`;
274
+ if (seenEdges.has(key))
275
+ continue;
276
+ seenEdges.add(key);
277
+ edges.push(normalized);
278
+ }
279
+ const nodes = Array.from(nodeIds).sort();
280
+ edges.sort((left, right) => left.from.localeCompare(right.from) ||
281
+ left.to.localeCompare(right.to) ||
282
+ left.type.localeCompare(right.type));
283
+ const dependentsByJobId = emptyGraphIndex(nodes);
284
+ const dependenciesByJobId = emptyGraphIndex(nodes);
285
+ for (const edge of edges) {
286
+ dependentsByJobId[edge.from]?.push(edge.to);
287
+ dependenciesByJobId[edge.to]?.push(edge.from);
288
+ }
289
+ sortGraphIndex(dependentsByJobId);
290
+ sortGraphIndex(dependenciesByJobId);
291
+ return {
292
+ nodes,
293
+ edges,
294
+ dependentsByJobId,
295
+ dependenciesByJobId,
296
+ roots: nodes.filter((node) => dependenciesByJobId[node]?.length === 0),
297
+ leaves: nodes.filter((node) => dependentsByJobId[node]?.length === 0),
298
+ issues
299
+ };
300
+ }
301
+ function normalizeSwarmCodexEdgeKind(kind) {
302
+ return kind === 'dependsOn' ? 'depends-on' : kind;
303
+ }
304
+ function normalizeSwarmCodexDecisionGraph(input, liveEvents = []) {
305
+ const nodes = input.nodes
306
+ .filter((node) => node && typeof node.id === 'string' && node.id)
307
+ .map((node) => normalizeSwarmCodexDecisionNode(node))
308
+ .sort((left, right) => left.id.localeCompare(right.id));
309
+ const edges = input.edges
310
+ .filter((edge) => edge && typeof edge.from === 'string' && typeof edge.to === 'string' && edge.from && edge.to)
311
+ .map((edge) => normalizeSwarmCodexDecisionEdge(edge))
312
+ .sort((left, right) => left.id.localeCompare(right.id));
313
+ const events = liveEvents
314
+ .map((event, index) => normalizeSwarmCodexDecisionEvent(event, index))
315
+ .sort((left, right) => left.generatedAt.localeCompare(right.generatedAt) || left.id.localeCompare(right.id));
316
+ const records = normalizeSwarmCodexDecisionRecords(nodes);
317
+ const snapshots = [
318
+ normalizeSwarmCodexDecisionSnapshot(input, nodes, edges, events, records)
319
+ ];
320
+ return {
321
+ kind: 'loom.decision-graph',
322
+ version: 1,
323
+ generatedAt: timestampToIso(input.generatedAt),
324
+ nodes,
325
+ edges,
326
+ events,
327
+ snapshots,
328
+ indexes: createDecisionGraphIndexes(nodes, edges),
329
+ records,
330
+ metadata: toJsonRecord({
331
+ sourceKind: input.kind,
332
+ sourceId: input.id,
333
+ runDir: input.runDir,
334
+ outDir: input.outDir,
335
+ summary: input.summary
336
+ })
337
+ };
338
+ }
339
+ function normalizeSwarmCodexDecisionNode(node) {
340
+ const kind = normalizeSwarmCodexDecisionNodeKind(node.kind);
341
+ const timestamp = typeof node.generatedAt === 'number' ? timestampToIso(node.generatedAt) : undefined;
342
+ const data = toJsonRecord(node.data);
343
+ const refs = stringRecord(node.refs);
344
+ return {
345
+ id: node.id,
346
+ kind,
347
+ ...(kind !== node.kind ? { sourceKind: node.kind } : {}),
348
+ ...(node.label ? { label: node.label } : {}),
349
+ ...(node.taskId ? { taskId: node.taskId } : {}),
350
+ ...(node.jobId ? { jobId: node.jobId } : {}),
351
+ ...(node.lane ? { lane: node.lane } : {}),
352
+ ...(node.model ? { model: node.model } : {}),
353
+ ...(node.computeId ? { computeId: node.computeId } : {}),
354
+ ...(node.modelTier ? { modelTier: node.modelTier } : {}),
355
+ ...(node.bucket ? { bucket: node.bucket } : {}),
356
+ ...(node.status ? { status: node.status } : {}),
357
+ ...(node.outcome ? { outcome: node.outcome } : {}),
358
+ ...(node.path ? { path: node.path } : {}),
359
+ ...(kind === 'worker' ? { workerId: node.jobId ?? node.id } : {}),
360
+ ...(kind === 'candidate' ? { candidateId: node.jobId ?? node.id } : {}),
361
+ ...(timestamp ? { createdAt: timestamp, updatedAt: timestamp } : {}),
362
+ ...(refs ? { refs } : {}),
363
+ ...(data ? { data } : {})
364
+ };
365
+ }
366
+ function normalizeSwarmCodexDecisionNodeKind(kind) {
367
+ if (kind === 'run')
368
+ return 'intent';
369
+ if (kind === 'job')
370
+ return 'worker';
371
+ if (kind === 'panel-projection')
372
+ return 'panel';
373
+ if (kind === 'improvement-loop')
374
+ return 'rsi';
375
+ return kind;
376
+ }
377
+ function normalizeSwarmCodexDecisionEdge(edge) {
378
+ const kind = normalizeSwarmCodexEdgeKind(edge.kind);
379
+ const data = toJsonRecord(edge.data);
380
+ return {
381
+ id: edge.id || `${kind}:${edge.from}->${edge.to}`,
382
+ kind,
383
+ from: edge.from,
384
+ to: edge.to,
385
+ ...(kind !== edge.kind ? { sourceKind: edge.kind } : {}),
386
+ ...(edge.label ? { label: edge.label } : {}),
387
+ ...(data ? { data } : {})
388
+ };
389
+ }
390
+ function normalizeSwarmCodexDecisionEvent(event, index) {
391
+ const nodes = (event.nodes ?? [])
392
+ .filter((node) => node && typeof node.id === 'string' && node.id)
393
+ .map((node) => normalizeSwarmCodexDecisionNode(node))
394
+ .sort((left, right) => left.id.localeCompare(right.id));
395
+ const edges = (event.edges ?? [])
396
+ .filter((edge) => edge && typeof edge.from === 'string' && typeof edge.to === 'string' && edge.from && edge.to)
397
+ .map((edge) => normalizeSwarmCodexDecisionEdge(edge))
398
+ .sort((left, right) => left.id.localeCompare(right.id));
399
+ const data = toJsonRecord(event.data);
400
+ return {
401
+ kind: 'loom.decision-graph.event',
402
+ version: 1,
403
+ id: decisionGraphEventId(event, index),
404
+ type: event.type,
405
+ generatedAt: timestampToIso(event.generatedAt),
406
+ ...(event.runId ? { runId: event.runId } : {}),
407
+ ...(event.taskId ? { taskId: event.taskId } : {}),
408
+ ...(event.jobId ? { jobId: event.jobId } : {}),
409
+ ...(event.lane ? { lane: event.lane } : {}),
410
+ ...(nodes.length ? { nodeIds: nodes.map((node) => node.id), nodes } : {}),
411
+ ...(edges.length ? { edgeIds: edges.map((edge) => edge.id), edges } : {}),
412
+ ...(data ? { data } : {})
413
+ };
414
+ }
415
+ function decisionGraphEventId(event, index) {
416
+ const scope = event.jobId ?? event.taskId ?? event.runId ?? 'event';
417
+ return `${scope}:${event.type}:${index}`;
418
+ }
419
+ function normalizeSwarmCodexDecisionSnapshot(input, nodes, edges, events, records) {
420
+ return {
421
+ kind: 'loom.decision-graph.snapshot',
422
+ version: 1,
423
+ id: `snapshot:${input.id}`,
424
+ generatedAt: timestampToIso(input.generatedAt),
425
+ label: 'current',
426
+ nodeIds: nodes.map((node) => node.id),
427
+ edgeIds: edges.map((edge) => edge.id),
428
+ eventIds: events.map((event) => event.id),
429
+ summary: {
430
+ nodes: nodes.length,
431
+ edges: edges.length,
432
+ events: events.length,
433
+ records: records.length
434
+ },
435
+ data: toJsonRecord({
436
+ sourceKind: input.kind,
437
+ sourceId: input.id,
438
+ runDir: input.runDir,
439
+ outDir: input.outDir
440
+ })
441
+ };
442
+ }
443
+ function normalizeSwarmCodexDecisionRecords(nodes) {
444
+ const records = [];
445
+ for (const node of nodes) {
446
+ if (node.kind === 'candidate') {
447
+ records.push({
448
+ kind: 'loom.decision-graph.merge-candidate',
449
+ id: `record:merge-candidate:${node.id}`,
450
+ nodeId: node.id,
451
+ taskId: node.taskId,
452
+ jobId: node.jobId,
453
+ lane: node.lane,
454
+ label: node.label,
455
+ status: node.status,
456
+ createdAt: node.createdAt,
457
+ updatedAt: node.updatedAt,
458
+ candidateId: node.candidateId ?? node.id,
459
+ sourceNodeId: node.id,
460
+ admissionStatus: admissionStatusFromNode(node),
461
+ admissionReasonCodes: admissionReasonsFromNode(node),
462
+ disposition: node.outcome,
463
+ data: node.data
464
+ });
465
+ }
466
+ else if (node.kind === 'evidence') {
467
+ records.push({
468
+ kind: 'loom.decision-graph.evidence',
469
+ id: `record:evidence:${node.id}`,
470
+ nodeId: node.id,
471
+ taskId: node.taskId,
472
+ jobId: node.jobId,
473
+ lane: node.lane,
474
+ label: node.label,
475
+ status: node.status,
476
+ createdAt: node.createdAt,
477
+ updatedAt: node.updatedAt,
478
+ evidenceKind: evidenceKindFromNode(node),
479
+ path: node.path,
480
+ producerNodeId: node.id,
481
+ data: node.data
482
+ });
483
+ }
484
+ else if (node.kind === 'gate') {
485
+ records.push({
486
+ kind: 'loom.decision-graph.gate',
487
+ id: `record:gate:${node.id}`,
488
+ nodeId: node.id,
489
+ taskId: node.taskId,
490
+ jobId: node.jobId,
491
+ lane: node.lane,
492
+ label: node.label,
493
+ status: node.status,
494
+ createdAt: node.createdAt,
495
+ updatedAt: node.updatedAt,
496
+ gateId: node.id,
497
+ data: node.data
498
+ });
499
+ }
500
+ else if (node.kind === 'replay') {
501
+ records.push({
502
+ kind: 'loom.decision-graph.replay',
503
+ id: `record:replay:${node.id}`,
504
+ nodeId: node.id,
505
+ taskId: node.taskId,
506
+ jobId: node.jobId,
507
+ lane: node.lane,
508
+ label: node.label,
509
+ status: node.status,
510
+ createdAt: node.createdAt,
511
+ updatedAt: node.updatedAt,
512
+ result: node.outcome,
513
+ data: node.data
514
+ });
515
+ }
516
+ else if (node.kind === 'rsi') {
517
+ records.push({
518
+ kind: 'loom.decision-graph.improvement-loop',
519
+ id: `record:improvement-loop:${node.id}`,
520
+ nodeId: node.id,
521
+ taskId: node.taskId,
522
+ jobId: node.jobId,
523
+ lane: node.lane,
524
+ label: node.label,
525
+ status: node.status,
526
+ createdAt: node.createdAt,
527
+ updatedAt: node.updatedAt,
528
+ loopId: node.id,
529
+ data: node.data
530
+ });
531
+ }
532
+ else if (node.kind === 'semantic-change') {
533
+ records.push({
534
+ kind: 'loom.decision-graph.semantic-change',
535
+ id: `record:semantic-change:${node.id}`,
536
+ nodeId: node.id,
537
+ taskId: node.taskId,
538
+ jobId: node.jobId,
539
+ lane: node.lane,
540
+ label: node.label,
541
+ status: node.status,
542
+ createdAt: node.createdAt,
543
+ updatedAt: node.updatedAt,
544
+ changeId: node.id,
545
+ conflictReason: stringFromData(node.data, 'conflictReason'),
546
+ data: node.data
547
+ });
548
+ }
549
+ else if (node.kind === 'tournament') {
550
+ records.push({
551
+ kind: 'loom.decision-graph.tournament',
552
+ id: `record:tournament:${node.id}`,
553
+ nodeId: node.id,
554
+ taskId: node.taskId,
555
+ jobId: node.jobId,
556
+ lane: node.lane,
557
+ label: node.label,
558
+ status: node.status,
559
+ createdAt: node.createdAt,
560
+ updatedAt: node.updatedAt,
561
+ candidateIds: stringArrayFromData(node.data, 'candidateIds'),
562
+ winnerCandidateId: stringFromData(node.data, 'winnerCandidateId'),
563
+ data: node.data
564
+ });
565
+ }
566
+ }
567
+ return records;
568
+ }
569
+ export function createLoomRunGraphPanelRecords(graph) {
570
+ const kinds = ['intent', 'decomposition', 'tournament', 'performance', 'evidence', 'merge', 'rsi'];
571
+ return kinds.map((panelKind) => ({
572
+ kind: 'loom.decision-graph.panel-projection',
573
+ id: `panel:${panelKind}`,
574
+ panelKind,
575
+ label: panelKind,
576
+ sourceNodeIds: panelSourceNodeIds(graph, panelKind),
577
+ sourceEdgeIds: panelSourceEdgeIds(graph, panelKind)
578
+ }));
579
+ }
580
+ function panelSourceNodeIds(graph, panelKind) {
581
+ const nodeKindsByPanel = {
582
+ intent: ['intent', 'task'],
583
+ decomposition: ['task', 'worker'],
584
+ tournament: ['tournament', 'candidate'],
585
+ performance: ['worker', 'gate', 'evidence'],
586
+ evidence: ['evidence', 'gate'],
587
+ merge: ['candidate', 'merge', 'decision', 'semantic-change'],
588
+ rsi: ['rsi']
589
+ };
590
+ const wanted = new Set(nodeKindsByPanel[panelKind] ?? []);
591
+ return graph.nodes.filter((node) => wanted.has(node.kind)).map((node) => node.id);
592
+ }
593
+ function panelSourceEdgeIds(graph, panelKind) {
594
+ const nodes = new Set(panelSourceNodeIds(graph, panelKind));
595
+ return graph.edges.filter((edge) => nodes.has(edge.from) || nodes.has(edge.to)).map((edge) => edge.id);
596
+ }
597
+ function summarizeTypedCounts(graph) {
598
+ const out = {};
599
+ for (const node of graph.nodes) {
600
+ if (node.kind === 'intent')
601
+ out.intents = (out.intents ?? 0) + 1;
602
+ else if (node.kind === 'task')
603
+ out.tasks = (out.tasks ?? 0) + 1;
604
+ else if (node.kind === 'worker')
605
+ out.workers = (out.workers ?? 0) + 1;
606
+ else if (node.kind === 'candidate')
607
+ out.candidates = (out.candidates ?? 0) + 1;
608
+ else if (node.kind === 'evidence')
609
+ out.evidence = (out.evidence ?? 0) + 1;
610
+ else if (node.kind === 'gate')
611
+ out.gates = (out.gates ?? 0) + 1;
612
+ else if (node.kind === 'decision')
613
+ out.decisions = (out.decisions ?? 0) + 1;
614
+ else if (node.kind === 'merge')
615
+ out.merges = (out.merges ?? 0) + 1;
616
+ else if (node.kind === 'replay')
617
+ out.replay = (out.replay ?? 0) + 1;
618
+ else if (node.kind === 'panel')
619
+ out.panels = (out.panels ?? 0) + 1;
620
+ else if (node.kind === 'tournament')
621
+ out.tournaments = (out.tournaments ?? 0) + 1;
622
+ else if (node.kind === 'rsi')
623
+ out.rsiLoops = (out.rsiLoops ?? 0) + 1;
624
+ else if (node.kind === 'semantic-change')
625
+ out.semanticChanges = (out.semanticChanges ?? 0) + 1;
626
+ }
627
+ for (const record of graph.records ?? []) {
628
+ if (record.kind === 'loom.decision-graph.panel-projection')
629
+ out.panels = Math.max(out.panels ?? 0, 1);
630
+ }
631
+ return out;
632
+ }
633
+ function createDecisionGraphIndexes(nodes, edges) {
634
+ const indexes = {
635
+ byNodeKind: {},
636
+ byEdgeKind: {},
637
+ byTaskId: {},
638
+ byJobId: {},
639
+ byLane: {}
640
+ };
641
+ for (const node of nodes) {
642
+ addIndexValue(indexes.byNodeKind, node.kind, node.id);
643
+ if (node.taskId)
644
+ addIndexValue(indexes.byTaskId, node.taskId, node.id);
645
+ if (node.jobId)
646
+ addIndexValue(indexes.byJobId, node.jobId, node.id);
647
+ if (node.lane)
648
+ addIndexValue(indexes.byLane, node.lane, node.id);
649
+ }
650
+ for (const edge of edges) {
651
+ addIndexValue(indexes.byEdgeKind, edge.kind, edge.id);
652
+ }
653
+ sortGraphIndex(indexes.byNodeKind);
654
+ sortGraphIndex(indexes.byEdgeKind);
655
+ sortGraphIndex(indexes.byTaskId);
656
+ sortGraphIndex(indexes.byJobId);
657
+ sortGraphIndex(indexes.byLane);
658
+ return indexes;
659
+ }
660
+ function addIndexValue(index, key, value) {
661
+ const values = index[key] ?? [];
662
+ if (!values.includes(value))
663
+ values.push(value);
664
+ index[key] = values;
665
+ }
666
+ function assertLoomDecisionGraph(value, file) {
667
+ if (!value || typeof value !== 'object')
668
+ throw new Error(`invalid ${file}: expected decisionGraph object`);
669
+ if (value.kind !== 'loom.decision-graph') {
670
+ throw new Error(`invalid ${file}: expected decisionGraph kind "loom.decision-graph"`);
671
+ }
672
+ if (value.version !== 1)
673
+ throw new Error(`invalid ${file}: expected decisionGraph version 1`);
674
+ if (!Array.isArray(value.nodes))
675
+ throw new Error(`invalid ${file}: expected decisionGraph nodes array`);
676
+ if (!Array.isArray(value.edges))
677
+ throw new Error(`invalid ${file}: expected decisionGraph edges array`);
678
+ if (!Array.isArray(value.events))
679
+ throw new Error(`invalid ${file}: expected decisionGraph events array`);
680
+ if (!Array.isArray(value.snapshots))
681
+ throw new Error(`invalid ${file}: expected decisionGraph snapshots array`);
682
+ }
683
+ function emptyGraphIndex(nodes) {
684
+ const out = {};
685
+ for (const node of nodes)
686
+ out[node] = [];
687
+ return out;
688
+ }
689
+ function sortGraphIndex(index) {
690
+ for (const values of Object.values(index)) {
691
+ values.sort();
692
+ }
693
+ }
694
+ function swarmRunGraphRunId(input) {
695
+ const basename = path.basename(input.runDir || '');
696
+ if (basename && basename !== path.sep)
697
+ return basename;
698
+ const idPart = input.id.split(':').filter(Boolean).at(-1);
699
+ return idPart || 'current';
700
+ }
701
+ function timestampToIso(value) {
702
+ const date = new Date(value);
703
+ return Number.isFinite(date.getTime()) ? date.toISOString() : nowIso();
704
+ }
705
+ function assertSwarmCodexRunGraph(value) {
706
+ if (!value || typeof value !== 'object')
707
+ throw new Error('invalid swarm run graph: expected object');
708
+ if (value.kind !== 'frontier.swarm-codex.run-graph') {
709
+ throw new Error('invalid swarm run graph: expected kind "frontier.swarm-codex.run-graph"');
710
+ }
711
+ if (value.version !== 1)
712
+ throw new Error('invalid swarm run graph: expected version 1');
713
+ if (!Array.isArray(value.nodes))
714
+ throw new Error('invalid swarm run graph: expected nodes array');
715
+ if (!Array.isArray(value.edges))
716
+ throw new Error('invalid swarm run graph: expected edges array');
717
+ }
718
+ function toJsonValue(value) {
719
+ if (value === null)
720
+ return null;
721
+ if (typeof value === 'boolean' || typeof value === 'string')
722
+ return value;
723
+ if (typeof value === 'number')
724
+ return Number.isFinite(value) ? value : null;
725
+ if (Array.isArray(value))
726
+ return value.map((item) => toJsonValue(item));
727
+ if (typeof value === 'object') {
728
+ const out = {};
729
+ for (const [key, item] of Object.entries(value)) {
730
+ if (item === undefined || typeof item === 'function' || typeof item === 'symbol')
731
+ continue;
732
+ out[key] = toJsonValue(item);
733
+ }
734
+ return out;
735
+ }
736
+ return String(value);
737
+ }
738
+ function toJsonRecord(value) {
739
+ const json = toJsonValue(value);
740
+ if (!json || typeof json !== 'object' || Array.isArray(json))
741
+ return undefined;
742
+ return Object.keys(json).length > 0 ? json : undefined;
743
+ }
744
+ function stringRecord(value) {
745
+ if (!value || typeof value !== 'object' || Array.isArray(value))
746
+ return undefined;
747
+ const out = {};
748
+ for (const [key, item] of Object.entries(value)) {
749
+ if (typeof item === 'string')
750
+ out[key] = item;
751
+ }
752
+ return Object.keys(out).length > 0 ? out : undefined;
753
+ }
754
+ function uniqueStrings(values) {
755
+ return Array.from(new Set(values.filter((value) => typeof value === 'string' && value.length > 0))).sort();
756
+ }
757
+ function uniqueEdges(edges) {
758
+ const seen = new Set();
759
+ const out = [];
760
+ for (const edge of edges) {
761
+ if (!edge.from || !edge.to || !edge.type)
762
+ continue;
763
+ const key = `${edge.type}\0${edge.from}\0${edge.to}`;
764
+ if (seen.has(key))
765
+ continue;
766
+ seen.add(key);
767
+ out.push({ from: edge.from, to: edge.to, type: edge.type });
768
+ }
769
+ return out.sort((left, right) => left.from.localeCompare(right.from) ||
770
+ left.to.localeCompare(right.to) ||
771
+ left.type.localeCompare(right.type));
772
+ }
773
+ function requiredNodes(label, nodes) {
774
+ const out = uniqueStrings(nodes);
775
+ if (out.length === 0)
776
+ throw new Error(`invalid run graph chunk: ${label} must not be empty`);
777
+ return out;
778
+ }
779
+ function pairwiseEdges(nodes, type) {
780
+ const edges = [];
781
+ for (let index = 0; index < nodes.length - 1; index += 1) {
782
+ edges.push({ from: nodes[index], to: nodes[index + 1], type });
783
+ }
784
+ return edges;
785
+ }
786
+ function normalizeChunkRoles(roles) {
787
+ const out = {};
788
+ for (const [key, values] of Object.entries(roles)) {
789
+ const normalized = uniqueStrings(values);
790
+ if (normalized.length > 0)
791
+ out[key] = normalized;
792
+ }
793
+ return out;
794
+ }
795
+ function stableChunkId(kind, nodes, edges) {
796
+ const edgePart = edges.map((edge) => `${edge.type}:${edge.from}->${edge.to}`).join('|');
797
+ return `chunk:${kind}:${[...nodes].join(',')}:${edgePart}`;
798
+ }
799
+ function admissionStatusFromNode(node) {
800
+ const value = stringFromData(node.data, 'admissionStatus') ?? stringFromData(node.data, 'mergeReadiness');
801
+ if (value === 'safe' || value === 'safe-with-losses' || value === 'review-required' || value === 'blocked') {
802
+ return value;
803
+ }
804
+ return undefined;
805
+ }
806
+ function admissionReasonsFromNode(node) {
807
+ const values = stringArrayFromData(node.data, 'admissionReasonCodes') ?? stringArrayFromData(node.data, 'reasonCodes');
808
+ const allowed = new Set([
809
+ 'missing-sidecar',
810
+ 'empty-sidecar',
811
+ 'stale-source-hash',
812
+ 'symbol-conflict',
813
+ 'effect-conflict',
814
+ 'lossy-import',
815
+ 'tests-missing'
816
+ ]);
817
+ const out = (values ?? []).filter((value) => allowed.has(value));
818
+ return out.length ? out : undefined;
819
+ }
820
+ function evidenceKindFromNode(node) {
821
+ return stringFromData(node.data, 'evidenceKind');
822
+ }
823
+ function stringFromData(data, key) {
824
+ const value = data?.[key];
825
+ return typeof value === 'string' && value ? value : undefined;
826
+ }
827
+ function stringArrayFromData(data, key) {
828
+ const value = data?.[key];
829
+ if (!Array.isArray(value))
830
+ return undefined;
831
+ const out = value.filter((item) => typeof item === 'string' && item.length > 0);
832
+ return out.length ? out : undefined;
833
+ }
11
834
  //# sourceMappingURL=graph.js.map