@shapeshift-labs/frontier-swarm 0.5.21 → 0.5.22
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/README.md +5 -1
- package/dist/index.d.ts +717 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2198 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRunDashboardSnapshot, createRunEdgeEvent, createRunEvent, createRunNodeEvent, defineRunArtifact, defineRunAttempt, defineRunDecision, defineRunEvidence, defineRunLane, defineRunPatch, defineRunTask, defineRunVerification, linkRunNodes, replayRunEvents } from '@shapeshift-labs/frontier-run';
|
|
1
2
|
export const FRONTIER_SWARM_MANIFEST_KIND = 'frontier.swarm.manifest';
|
|
2
3
|
export const FRONTIER_SWARM_MANIFEST_VERSION = 1;
|
|
3
4
|
export const FRONTIER_SWARM_TASK_KIND = 'frontier.swarm.task';
|
|
@@ -10,6 +11,22 @@ export const FRONTIER_SWARM_EVENT_KIND = 'frontier.swarm.event';
|
|
|
10
11
|
export const FRONTIER_SWARM_EVENT_VERSION = 1;
|
|
11
12
|
export const FRONTIER_SWARM_EVENT_STREAM_KIND = 'frontier.swarm.event-stream';
|
|
12
13
|
export const FRONTIER_SWARM_EVENT_STREAM_VERSION = 1;
|
|
14
|
+
export const FRONTIER_SWARM_RUN_GRAPH_KIND = 'frontier.swarm.run-graph';
|
|
15
|
+
export const FRONTIER_SWARM_RUN_GRAPH_VERSION = 1;
|
|
16
|
+
export const FRONTIER_SWARM_GRAPH_SNAPSHOT_KIND = 'frontier.swarm.graph-snapshot';
|
|
17
|
+
export const FRONTIER_SWARM_GRAPH_SNAPSHOT_VERSION = 1;
|
|
18
|
+
export const FRONTIER_SWARM_RUN_GRAPH_CHUNK_KIND = 'frontier.swarm.run-graph-chunk';
|
|
19
|
+
export const FRONTIER_SWARM_RUN_GRAPH_CHUNK_VERSION = 1;
|
|
20
|
+
export const FRONTIER_SWARM_GATE_RECORD_KIND = 'frontier.swarm.gate-record';
|
|
21
|
+
export const FRONTIER_SWARM_GATE_RECORD_VERSION = 1;
|
|
22
|
+
export const FRONTIER_SWARM_EVIDENCE_RECORD_KIND = 'frontier.swarm.evidence-record';
|
|
23
|
+
export const FRONTIER_SWARM_EVIDENCE_RECORD_VERSION = 1;
|
|
24
|
+
export const FRONTIER_SWARM_PATCH_EVENT_KIND = 'frontier.swarm.patch-event';
|
|
25
|
+
export const FRONTIER_SWARM_PATCH_EVENT_VERSION = 1;
|
|
26
|
+
export const FRONTIER_SWARM_REPLAY_RECORD_KIND = 'frontier.swarm.replay-record';
|
|
27
|
+
export const FRONTIER_SWARM_REPLAY_RECORD_VERSION = 1;
|
|
28
|
+
export const FRONTIER_SWARM_IMPROVEMENT_LOOP_KIND = 'frontier.swarm.improvement-loop';
|
|
29
|
+
export const FRONTIER_SWARM_IMPROVEMENT_LOOP_VERSION = 1;
|
|
13
30
|
export const FRONTIER_SWARM_MAILBOX_KIND = 'frontier.swarm.mailbox';
|
|
14
31
|
export const FRONTIER_SWARM_MAILBOX_VERSION = 1;
|
|
15
32
|
export const FRONTIER_SWARM_PROOF_KIND = 'frontier.swarm.proof';
|
|
@@ -30,6 +47,10 @@ export const FRONTIER_SWARM_MERGE_PLAN_KIND = 'frontier.swarm.merge-plan';
|
|
|
30
47
|
export const FRONTIER_SWARM_MERGE_PLAN_VERSION = 1;
|
|
31
48
|
export const FRONTIER_SWARM_MERGE_BUNDLE_KIND = 'frontier.swarm.merge-bundle';
|
|
32
49
|
export const FRONTIER_SWARM_MERGE_BUNDLE_VERSION = 1;
|
|
50
|
+
export const FRONTIER_SWARM_SEMANTIC_CHANGE_KIND = 'frontier.swarm.semantic-change';
|
|
51
|
+
export const FRONTIER_SWARM_SEMANTIC_CHANGE_VERSION = 1;
|
|
52
|
+
export const FRONTIER_SWARM_MERGE_CANDIDATE_KIND = 'frontier.swarm.merge-candidate';
|
|
53
|
+
export const FRONTIER_SWARM_MERGE_CANDIDATE_VERSION = 1;
|
|
33
54
|
export const FRONTIER_SWARM_QUEUE_OVERLAY_KIND = 'frontier.swarm.queue-overlay';
|
|
34
55
|
export const FRONTIER_SWARM_QUEUE_OVERLAY_VERSION = 1;
|
|
35
56
|
export const FRONTIER_SWARM_MERGE_INDEX_KIND = 'frontier.swarm.merge-index';
|
|
@@ -96,6 +117,8 @@ export const FRONTIER_SWARM_QUEUE_OUTCOME_MODEL_KIND = 'frontier.swarm.queue-out
|
|
|
96
117
|
export const FRONTIER_SWARM_QUEUE_OUTCOME_MODEL_VERSION = 1;
|
|
97
118
|
export const FRONTIER_SWARM_TERMINAL_STATE_RECONCILIATION_KIND = 'frontier.swarm.terminal-state-reconciliation';
|
|
98
119
|
export const FRONTIER_SWARM_TERMINAL_STATE_RECONCILIATION_VERSION = 1;
|
|
120
|
+
export const FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_KIND = 'frontier.swarm.terminal-outcome-record';
|
|
121
|
+
export const FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_VERSION = 1;
|
|
99
122
|
export const FRONTIER_SWARM_PRIORITY_POLICY_KIND = 'frontier.swarm.priority-policy';
|
|
100
123
|
export const FRONTIER_SWARM_PRIORITY_POLICY_VERSION = 1;
|
|
101
124
|
export const FRONTIER_SWARM_MODEL_ROUTE_KIND = 'frontier.swarm.model-route';
|
|
@@ -118,6 +141,42 @@ export const FRONTIER_SWARM_VERIFICATION_CATEGORY_HINTS = [
|
|
|
118
141
|
'browser',
|
|
119
142
|
'oracle'
|
|
120
143
|
];
|
|
144
|
+
export const FRONTIER_SWARM_GRAPH_NODE_KINDS = [
|
|
145
|
+
'intent',
|
|
146
|
+
'task',
|
|
147
|
+
'worker',
|
|
148
|
+
'candidate',
|
|
149
|
+
'evidence',
|
|
150
|
+
'gate',
|
|
151
|
+
'decision',
|
|
152
|
+
'merge',
|
|
153
|
+
'replay',
|
|
154
|
+
'rsi'
|
|
155
|
+
];
|
|
156
|
+
export const FRONTIER_SWARM_GRAPH_EDGE_KINDS = [
|
|
157
|
+
'dependsOn',
|
|
158
|
+
'blocks',
|
|
159
|
+
'produces',
|
|
160
|
+
'verifies',
|
|
161
|
+
'conflictsWith',
|
|
162
|
+
'supersedes',
|
|
163
|
+
'mergesInto'
|
|
164
|
+
];
|
|
165
|
+
export const FRONTIER_SWARM_MERGE_CANDIDATE_ADMISSION_STATUSES = [
|
|
166
|
+
'safe',
|
|
167
|
+
'safe-with-losses',
|
|
168
|
+
'review-required',
|
|
169
|
+
'blocked'
|
|
170
|
+
];
|
|
171
|
+
export const FRONTIER_SWARM_MERGE_CANDIDATE_REASON_CODES = [
|
|
172
|
+
'missing-sidecar',
|
|
173
|
+
'empty-sidecar',
|
|
174
|
+
'stale-source-hash',
|
|
175
|
+
'symbol-conflict',
|
|
176
|
+
'effect-conflict',
|
|
177
|
+
'lossy-import',
|
|
178
|
+
'tests-missing'
|
|
179
|
+
];
|
|
121
180
|
export const FRONTIER_SWARM_SEMANTIC_OWNERSHIP_EXPORT_STABLE_KEY_KIND = 'exported-declaration';
|
|
122
181
|
export const FRONTIER_SWARM_SEMANTIC_OWNERSHIP_NAMESPACE_EXPORT_STABLE_KEY_KIND = 'namespace-export';
|
|
123
182
|
export const FRONTIER_SWARM_SEMANTIC_OWNERSHIP_TYPE_STABLE_KEY_KIND = 'type-declaration';
|
|
@@ -176,6 +235,7 @@ export const FRONTIER_SWARM_TERMINAL_OUTCOME_LABELS = [
|
|
|
176
235
|
'superseded',
|
|
177
236
|
'evidence-only',
|
|
178
237
|
'no-change',
|
|
238
|
+
'research-complete',
|
|
179
239
|
'generated-by-collector',
|
|
180
240
|
'patch-missing',
|
|
181
241
|
'bundle-missing',
|
|
@@ -186,6 +246,26 @@ export const FRONTIER_SWARM_TERMINAL_OUTCOME_LABELS = [
|
|
|
186
246
|
'human-blocked',
|
|
187
247
|
'coordinator-review'
|
|
188
248
|
];
|
|
249
|
+
export const FRONTIER_SWARM_TERMINAL_OUTCOME_STATUSES = [
|
|
250
|
+
'applied',
|
|
251
|
+
'rejected',
|
|
252
|
+
'superseded',
|
|
253
|
+
'no-change',
|
|
254
|
+
'conflict',
|
|
255
|
+
'human-needed',
|
|
256
|
+
'research-complete',
|
|
257
|
+
'rerun'
|
|
258
|
+
];
|
|
259
|
+
export const FRONTIER_SWARM_TERMINAL_OUTCOME_REASON_CODES = [
|
|
260
|
+
'accepted-by-admission',
|
|
261
|
+
'failed-verification',
|
|
262
|
+
'superseded-by-newer-output',
|
|
263
|
+
'no-effective-change',
|
|
264
|
+
'conflict-detected',
|
|
265
|
+
'human-decision-required',
|
|
266
|
+
'research-complete',
|
|
267
|
+
'stale-rerun-required'
|
|
268
|
+
];
|
|
189
269
|
const DEFAULT_COMPLETED_STATUSES = ['completed', 'verified', 'done', 'verified-local-harness'];
|
|
190
270
|
const DEFAULT_SWARM_EVENT_TYPES = [
|
|
191
271
|
'swarm.started',
|
|
@@ -378,6 +458,1016 @@ export function createSwarmTaskSelection(manifestInput, taskInput, options = {})
|
|
|
378
458
|
summary: summarizeTaskSelection(entries)
|
|
379
459
|
};
|
|
380
460
|
}
|
|
461
|
+
export function createSwarmRunGraph(input = {}) {
|
|
462
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
463
|
+
const nodes = dedupeGraphNodes((input.nodes ?? []).map((node) => normalizeGraphNode(node, generatedAt)));
|
|
464
|
+
const knownNodeIds = new Set(nodes.map((node) => node.id));
|
|
465
|
+
const edges = dedupeGraphEdges((input.edges ?? [])
|
|
466
|
+
.map((edge) => normalizeGraphEdge(edge, generatedAt))
|
|
467
|
+
.filter((edge) => knownNodeIds.has(edge.from) && knownNodeIds.has(edge.to)));
|
|
468
|
+
const knownEdgeIds = new Set(edges.map((edge) => edge.id));
|
|
469
|
+
const events = (input.events ?? [])
|
|
470
|
+
.map((event) => normalizeGraphEvent(event, generatedAt))
|
|
471
|
+
.filter((event) => event.nodeId === undefined || knownNodeIds.has(event.nodeId))
|
|
472
|
+
.filter((event) => event.edgeId === undefined || knownEdgeIds.has(event.edgeId))
|
|
473
|
+
.sort((left, right) => left.timestamp - right.timestamp || left.id.localeCompare(right.id));
|
|
474
|
+
const graph = {
|
|
475
|
+
kind: FRONTIER_SWARM_RUN_GRAPH_KIND,
|
|
476
|
+
version: FRONTIER_SWARM_RUN_GRAPH_VERSION,
|
|
477
|
+
id: input.id ?? 'swarm-run-graph:' + stableHash([input.runId, nodes.map((node) => node.id), edges.map((edge) => edge.id)]),
|
|
478
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
479
|
+
...(input.title ? { title: input.title } : {}),
|
|
480
|
+
generatedAt,
|
|
481
|
+
nodes,
|
|
482
|
+
edges,
|
|
483
|
+
events,
|
|
484
|
+
summary: summarizeSwarmRunGraph(nodes, edges, events),
|
|
485
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
486
|
+
};
|
|
487
|
+
return graph;
|
|
488
|
+
}
|
|
489
|
+
export function normalizeSwarmRunGraph(input) {
|
|
490
|
+
if (input.kind === FRONTIER_SWARM_RUN_GRAPH_KIND) {
|
|
491
|
+
const graph = input;
|
|
492
|
+
return createSwarmRunGraph({
|
|
493
|
+
id: graph.id,
|
|
494
|
+
runId: graph.runId,
|
|
495
|
+
title: graph.title,
|
|
496
|
+
generatedAt: graph.generatedAt,
|
|
497
|
+
nodes: graph.nodes,
|
|
498
|
+
edges: graph.edges,
|
|
499
|
+
events: graph.events,
|
|
500
|
+
metadata: graph.metadata
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return createSwarmRunGraph(input);
|
|
504
|
+
}
|
|
505
|
+
export function createSwarmGraphSnapshot(input) {
|
|
506
|
+
const graph = normalizeSwarmRunGraph(input.graph);
|
|
507
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
508
|
+
return {
|
|
509
|
+
kind: FRONTIER_SWARM_GRAPH_SNAPSHOT_KIND,
|
|
510
|
+
version: FRONTIER_SWARM_GRAPH_SNAPSHOT_VERSION,
|
|
511
|
+
id: input.id ?? 'swarm-graph-snapshot:' + stableHash([graph.id, generatedAt, graph.summary]),
|
|
512
|
+
graphId: graph.id,
|
|
513
|
+
generatedAt,
|
|
514
|
+
summary: graph.summary,
|
|
515
|
+
graph,
|
|
516
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
export function createSwarmGateRecord(input) {
|
|
520
|
+
const metrics = toJsonObject(input.metrics);
|
|
521
|
+
const metadata = toJsonObject(input.metadata);
|
|
522
|
+
return {
|
|
523
|
+
kind: FRONTIER_SWARM_GATE_RECORD_KIND,
|
|
524
|
+
version: FRONTIER_SWARM_GATE_RECORD_VERSION,
|
|
525
|
+
id: input.id ?? 'swarm-gate:' + stableHash([input.type, input.command, input.path, input.jobId, input.taskId, input.startedAt, input.finishedAt]),
|
|
526
|
+
type: input.type,
|
|
527
|
+
status: input.status ?? 'unknown',
|
|
528
|
+
required: input.required ?? false,
|
|
529
|
+
...(input.command ? { command: input.command } : {}),
|
|
530
|
+
...(input.path ? { path: input.path } : {}),
|
|
531
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
532
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
533
|
+
...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}),
|
|
534
|
+
...(input.finishedAt !== undefined ? { finishedAt: input.finishedAt } : {}),
|
|
535
|
+
...(input.failureReason ? { failureReason: input.failureReason } : {}),
|
|
536
|
+
...(input.confidence ? { confidence: input.confidence } : {}),
|
|
537
|
+
...(metrics ? { metrics } : {}),
|
|
538
|
+
...(metadata ? { metadata } : {})
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
export function createSwarmEvidenceRecord(input) {
|
|
542
|
+
const metrics = toJsonObject(input.metrics);
|
|
543
|
+
const metadata = toJsonObject(input.metadata);
|
|
544
|
+
const producedAt = input.producedAt ?? Date.now();
|
|
545
|
+
return {
|
|
546
|
+
kind: FRONTIER_SWARM_EVIDENCE_RECORD_KIND,
|
|
547
|
+
version: FRONTIER_SWARM_EVIDENCE_RECORD_VERSION,
|
|
548
|
+
id: input.id ?? 'swarm-evidence:' + stableHash([input.type, input.path, input.ref, input.jobId, input.taskId, input.gateId]),
|
|
549
|
+
type: input.type,
|
|
550
|
+
...(input.path ? { path: input.path } : {}),
|
|
551
|
+
...(input.ref ? { ref: input.ref } : {}),
|
|
552
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
553
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
554
|
+
...(input.gateId ? { gateId: input.gateId } : {}),
|
|
555
|
+
producedAt,
|
|
556
|
+
...(input.confidence ? { confidence: input.confidence } : {}),
|
|
557
|
+
...(metrics ? { metrics } : {}),
|
|
558
|
+
...(metadata ? { metadata } : {})
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
export function createSwarmPatchEvent(input) {
|
|
562
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
563
|
+
const graphRefs = normalizeSwarmGraphRefs(input.graphRefs ?? []);
|
|
564
|
+
const sequence = input.sequence !== undefined ? Math.max(0, Math.floor(input.sequence)) : undefined;
|
|
565
|
+
return {
|
|
566
|
+
kind: FRONTIER_SWARM_PATCH_EVENT_KIND,
|
|
567
|
+
version: FRONTIER_SWARM_PATCH_EVENT_VERSION,
|
|
568
|
+
id: input.id ?? 'swarm-patch-event:' + stableHash([input.replayRecordId, sequence, input.operation, input.path, input.from, input.hash]),
|
|
569
|
+
operation: input.operation,
|
|
570
|
+
path: input.path,
|
|
571
|
+
...(input.from ? { from: input.from } : {}),
|
|
572
|
+
...(input.hash ? { hash: input.hash } : {}),
|
|
573
|
+
...(sequence !== undefined ? { sequence } : {}),
|
|
574
|
+
...(input.value !== undefined ? { value: toJsonValue(input.value) } : {}),
|
|
575
|
+
...(input.before !== undefined ? { before: toJsonValue(input.before) } : {}),
|
|
576
|
+
...(input.after !== undefined ? { after: toJsonValue(input.after) } : {}),
|
|
577
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
578
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
579
|
+
...(input.replayRecordId ? { replayRecordId: input.replayRecordId } : {}),
|
|
580
|
+
graphRefs,
|
|
581
|
+
generatedAt,
|
|
582
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
export function createSwarmReplayRecord(input = {}) {
|
|
586
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
587
|
+
const title = input.title ?? titleFromId(input.id ?? input.subject ?? input.replayBundleId ?? 'replay record');
|
|
588
|
+
const evidenceRefs = normalizeNamedRefs(input.evidenceRefs ?? [], 'evidence');
|
|
589
|
+
const graphRefs = normalizeSwarmGraphRefs(input.graphRefs ?? []);
|
|
590
|
+
const replayId = input.id ?? 'swarm-replay-record:' + stableHash([
|
|
591
|
+
title,
|
|
592
|
+
input.status,
|
|
593
|
+
input.subject,
|
|
594
|
+
input.replayBundleId,
|
|
595
|
+
input.patchEvents ?? [],
|
|
596
|
+
evidenceRefs,
|
|
597
|
+
graphRefs,
|
|
598
|
+
input.jobId,
|
|
599
|
+
input.taskId
|
|
600
|
+
]);
|
|
601
|
+
const patchEvents = (input.patchEvents ?? [])
|
|
602
|
+
.map((event, index) => normalizeSwarmPatchEvent(event, replayId, index))
|
|
603
|
+
.sort((left, right) => (left.sequence ?? Number.MAX_SAFE_INTEGER) - (right.sequence ?? Number.MAX_SAFE_INTEGER) || left.id.localeCompare(right.id));
|
|
604
|
+
return {
|
|
605
|
+
kind: FRONTIER_SWARM_REPLAY_RECORD_KIND,
|
|
606
|
+
version: FRONTIER_SWARM_REPLAY_RECORD_VERSION,
|
|
607
|
+
id: replayId,
|
|
608
|
+
title,
|
|
609
|
+
status: input.status ?? 'unknown',
|
|
610
|
+
...(input.subject ? { subject: input.subject } : {}),
|
|
611
|
+
...(input.replayBundleId ? { replayBundleId: input.replayBundleId } : {}),
|
|
612
|
+
patchEvents,
|
|
613
|
+
evidenceRefs,
|
|
614
|
+
graphRefs,
|
|
615
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
616
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
617
|
+
...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}),
|
|
618
|
+
...(input.finishedAt !== undefined ? { finishedAt: input.finishedAt } : {}),
|
|
619
|
+
generatedAt,
|
|
620
|
+
summary: {
|
|
621
|
+
patchEventCount: patchEvents.length,
|
|
622
|
+
evidenceRefCount: evidenceRefs.length,
|
|
623
|
+
graphRefCount: graphRefs.length
|
|
624
|
+
},
|
|
625
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
export function createSwarmImprovementLoop(input = {}) {
|
|
629
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
630
|
+
const title = input.title ?? titleFromId(input.id ?? input.subject ?? 'improvement loop');
|
|
631
|
+
const replayRecordIds = uniqueStrings(input.replayRecordIds ?? []);
|
|
632
|
+
const patchEventIds = uniqueStrings(input.patchEventIds ?? []);
|
|
633
|
+
const evidenceRefs = normalizeNamedRefs(input.evidenceRefs ?? [], 'evidence');
|
|
634
|
+
const graphRefs = normalizeSwarmGraphRefs(input.graphRefs ?? []);
|
|
635
|
+
return {
|
|
636
|
+
kind: FRONTIER_SWARM_IMPROVEMENT_LOOP_KIND,
|
|
637
|
+
version: FRONTIER_SWARM_IMPROVEMENT_LOOP_VERSION,
|
|
638
|
+
id: input.id ?? 'swarm-improvement-loop:' + stableHash([
|
|
639
|
+
title,
|
|
640
|
+
input.status,
|
|
641
|
+
input.subject,
|
|
642
|
+
input.observation,
|
|
643
|
+
input.action,
|
|
644
|
+
input.result,
|
|
645
|
+
replayRecordIds,
|
|
646
|
+
patchEventIds,
|
|
647
|
+
evidenceRefs,
|
|
648
|
+
graphRefs,
|
|
649
|
+
input.jobId,
|
|
650
|
+
input.taskId
|
|
651
|
+
]),
|
|
652
|
+
title,
|
|
653
|
+
status: input.status ?? inferImprovementLoopStatus(input),
|
|
654
|
+
...(input.subject ? { subject: input.subject } : {}),
|
|
655
|
+
...(input.observation !== undefined ? { observation: toJsonValue(input.observation) } : {}),
|
|
656
|
+
...(input.action !== undefined ? { action: toJsonValue(input.action) } : {}),
|
|
657
|
+
...(input.result !== undefined ? { result: toJsonValue(input.result) } : {}),
|
|
658
|
+
replayRecordIds,
|
|
659
|
+
patchEventIds,
|
|
660
|
+
evidenceRefs,
|
|
661
|
+
graphRefs,
|
|
662
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
663
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
664
|
+
generatedAt,
|
|
665
|
+
summary: {
|
|
666
|
+
replayRecordCount: replayRecordIds.length,
|
|
667
|
+
patchEventCount: patchEventIds.length,
|
|
668
|
+
evidenceRefCount: evidenceRefs.length,
|
|
669
|
+
graphRefCount: graphRefs.length
|
|
670
|
+
},
|
|
671
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
export function mapSwarmGateEvidenceToGraph(input) {
|
|
675
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
676
|
+
const gateRecords = (input.gates ?? []).map((gate) => isSwarmGateRecord(gate) ? gate : createSwarmGateRecord(gate));
|
|
677
|
+
const evidenceRecords = (input.evidence ?? []).map((evidence) => isSwarmEvidenceRecord(evidence) ? evidence : createSwarmEvidenceRecord(evidence));
|
|
678
|
+
const nodes = [
|
|
679
|
+
...gateRecords.map((gate) => ({
|
|
680
|
+
id: graphNodeId('gate', gate.id),
|
|
681
|
+
kind: 'gate',
|
|
682
|
+
title: gate.command ?? gate.path ?? gate.type,
|
|
683
|
+
status: gate.status,
|
|
684
|
+
jobId: gate.jobId,
|
|
685
|
+
taskId: gate.taskId,
|
|
686
|
+
path: gate.path,
|
|
687
|
+
generatedAt,
|
|
688
|
+
metadata: gate
|
|
689
|
+
})),
|
|
690
|
+
...evidenceRecords.map((evidence) => ({
|
|
691
|
+
id: graphNodeId('evidence', evidence.id),
|
|
692
|
+
kind: 'evidence',
|
|
693
|
+
title: evidence.path ?? evidence.ref ?? evidence.type,
|
|
694
|
+
status: evidence.confidence,
|
|
695
|
+
jobId: evidence.jobId,
|
|
696
|
+
taskId: evidence.taskId,
|
|
697
|
+
path: evidence.path,
|
|
698
|
+
generatedAt,
|
|
699
|
+
metadata: evidence
|
|
700
|
+
}))
|
|
701
|
+
];
|
|
702
|
+
const edges = [];
|
|
703
|
+
for (const evidence of evidenceRecords) {
|
|
704
|
+
if (evidence.gateId) {
|
|
705
|
+
edges.push({
|
|
706
|
+
kind: 'produces',
|
|
707
|
+
from: graphNodeId('gate', evidence.gateId),
|
|
708
|
+
to: graphNodeId('evidence', evidence.id),
|
|
709
|
+
generatedAt
|
|
710
|
+
});
|
|
711
|
+
edges.push({
|
|
712
|
+
kind: 'verifies',
|
|
713
|
+
from: graphNodeId('evidence', evidence.id),
|
|
714
|
+
to: graphNodeId('gate', evidence.gateId),
|
|
715
|
+
generatedAt
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return createSwarmRunGraph({
|
|
720
|
+
runId: input.runId,
|
|
721
|
+
title: 'Gate and evidence graph',
|
|
722
|
+
generatedAt,
|
|
723
|
+
nodes,
|
|
724
|
+
edges
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
export function createSwarmGateEvidenceGraph(input) {
|
|
728
|
+
return mapSwarmGateEvidenceToGraph(input);
|
|
729
|
+
}
|
|
730
|
+
export function classifySwarmMergeCandidateAdmission(input = {}) {
|
|
731
|
+
const arrayInput = Array.isArray(input);
|
|
732
|
+
const objectInput = arrayInput
|
|
733
|
+
? undefined
|
|
734
|
+
: input;
|
|
735
|
+
const status = objectInput ? normalizeOptionalString(objectInput.status) : undefined;
|
|
736
|
+
if (status)
|
|
737
|
+
return status;
|
|
738
|
+
const reasonCodes = arrayInput
|
|
739
|
+
? normalizeSwarmMergeCandidateReasonCodes(input)
|
|
740
|
+
: normalizeSwarmMergeCandidateReasonCodes(objectInput?.reasonCodes ?? []);
|
|
741
|
+
if (reasonCodes.some((reason) => FRONTIER_SWARM_BLOCKING_MERGE_CANDIDATE_REASON_CODES.has(reason)))
|
|
742
|
+
return 'blocked';
|
|
743
|
+
if (reasonCodes.some((reason) => FRONTIER_SWARM_REVIEW_MERGE_CANDIDATE_REASON_CODES.has(reason)))
|
|
744
|
+
return 'review-required';
|
|
745
|
+
if (reasonCodes.includes('lossy-import'))
|
|
746
|
+
return 'safe-with-losses';
|
|
747
|
+
return 'safe';
|
|
748
|
+
}
|
|
749
|
+
export function createSwarmSemanticChange(input) {
|
|
750
|
+
if (isSwarmSemanticChange(input))
|
|
751
|
+
return cloneJsonValue(input);
|
|
752
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
753
|
+
const symbolId = normalizeId(input.symbolId, 'semantic change symbol id');
|
|
754
|
+
const declarationKind = normalizeId(input.declarationKind, 'semantic change declaration kind');
|
|
755
|
+
const sourceSpan = normalizeSwarmSourceSpan(input.sourceSpan, {
|
|
756
|
+
sourcePath: input.sourcePath,
|
|
757
|
+
sourceHash: input.sourceHash,
|
|
758
|
+
expectedSourceHash: input.expectedSourceHash
|
|
759
|
+
});
|
|
760
|
+
const operation = normalizeOptionalString(input.operation) ?? 'unknown';
|
|
761
|
+
const derivedReasonCodes = deriveSwarmSemanticChangeReasonCodes(input, sourceSpan);
|
|
762
|
+
const reasonCodes = normalizeSwarmMergeCandidateReasonCodes([...(input.reasonCodes ?? []), ...derivedReasonCodes]);
|
|
763
|
+
const conflictReason = normalizeSwarmConflictReason(input.conflictReason, reasonCodes);
|
|
764
|
+
return {
|
|
765
|
+
kind: FRONTIER_SWARM_SEMANTIC_CHANGE_KIND,
|
|
766
|
+
version: FRONTIER_SWARM_SEMANTIC_CHANGE_VERSION,
|
|
767
|
+
id: input.id ?? 'swarm-semantic-change:' + stableHash([symbolId, declarationKind, sourceSpan, operation, reasonCodes]),
|
|
768
|
+
symbolId,
|
|
769
|
+
declarationKind,
|
|
770
|
+
sourceSpan,
|
|
771
|
+
operation,
|
|
772
|
+
confidence: normalizeSwarmSemanticConfidence(input.confidence),
|
|
773
|
+
...(conflictReason ? { conflictReason } : {}),
|
|
774
|
+
status: classifySwarmMergeCandidateAdmission({ status: input.status, reasonCodes }),
|
|
775
|
+
reasonCodes,
|
|
776
|
+
generatedAt,
|
|
777
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
export function createSwarmMergeCandidate(input) {
|
|
781
|
+
if (isSwarmMergeCandidate(input))
|
|
782
|
+
return cloneJsonValue(input);
|
|
783
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
784
|
+
const shorthandChange = input.symbolId || input.declarationKind || input.operation || input.sourceSpan || input.sourcePath
|
|
785
|
+
? [{
|
|
786
|
+
symbolId: input.symbolId ?? 'unknown-symbol',
|
|
787
|
+
declarationKind: input.declarationKind ?? 'unknown',
|
|
788
|
+
operation: input.operation ?? 'unknown',
|
|
789
|
+
sourceSpan: input.sourceSpan,
|
|
790
|
+
sourcePath: input.sourcePath,
|
|
791
|
+
sourceHash: input.sourceHash,
|
|
792
|
+
expectedSourceHash: input.expectedSourceHash,
|
|
793
|
+
confidence: input.confidence,
|
|
794
|
+
conflictReason: input.conflictReason,
|
|
795
|
+
status: input.status,
|
|
796
|
+
reasonCodes: input.reasonCodes,
|
|
797
|
+
generatedAt
|
|
798
|
+
}]
|
|
799
|
+
: [];
|
|
800
|
+
const semanticChanges = [...(input.semanticChanges ?? []), ...(input.changes ?? []), ...shorthandChange]
|
|
801
|
+
.map((change) => createSwarmSemanticChange({ ...change, generatedAt: change.generatedAt ?? generatedAt }));
|
|
802
|
+
const primaryChange = semanticChanges[0];
|
|
803
|
+
const candidateSourceSpan = primaryChange?.sourceSpan ?? normalizeSwarmSourceSpan(input.sourceSpan, {
|
|
804
|
+
sourcePath: input.sourcePath,
|
|
805
|
+
sourceHash: input.sourceHash,
|
|
806
|
+
expectedSourceHash: input.expectedSourceHash
|
|
807
|
+
});
|
|
808
|
+
const derivedReasonCodes = deriveSwarmMergeCandidateReasonCodes(input, semanticChanges, candidateSourceSpan);
|
|
809
|
+
const reasonCodes = normalizeSwarmMergeCandidateReasonCodes([
|
|
810
|
+
...(input.reasonCodes ?? []),
|
|
811
|
+
...semanticChanges.flatMap((change) => change.reasonCodes),
|
|
812
|
+
...derivedReasonCodes
|
|
813
|
+
]);
|
|
814
|
+
const symbolIds = uniqueStrings([
|
|
815
|
+
input.symbolId,
|
|
816
|
+
...semanticChanges.map((change) => change.symbolId)
|
|
817
|
+
]);
|
|
818
|
+
const declarationKinds = uniqueStrings([
|
|
819
|
+
input.declarationKind,
|
|
820
|
+
...semanticChanges.map((change) => change.declarationKind)
|
|
821
|
+
]);
|
|
822
|
+
const changedPaths = uniqueStrings(semanticChanges.map((change) => change.sourceSpan.path).concat(candidateSourceSpan.path));
|
|
823
|
+
const conflictReason = normalizeSwarmConflictReason(input.conflictReason, reasonCodes);
|
|
824
|
+
const metadata = toJsonObject(input.metadata);
|
|
825
|
+
return {
|
|
826
|
+
kind: FRONTIER_SWARM_MERGE_CANDIDATE_KIND,
|
|
827
|
+
version: FRONTIER_SWARM_MERGE_CANDIDATE_VERSION,
|
|
828
|
+
id: input.id ?? 'swarm-merge-candidate:' + stableHash([input.jobId, input.taskId, symbolIds, declarationKinds, semanticChanges, reasonCodes]),
|
|
829
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
830
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
831
|
+
...(input.lane ? { lane: input.lane } : {}),
|
|
832
|
+
...(input.title ? { title: input.title } : {}),
|
|
833
|
+
...(input.sidecarPath ? { sidecarPath: input.sidecarPath } : {}),
|
|
834
|
+
symbolId: symbolIds[0] ?? primaryChange?.symbolId ?? 'unknown-symbol',
|
|
835
|
+
symbolIds,
|
|
836
|
+
declarationKind: declarationKinds[0] ?? primaryChange?.declarationKind ?? 'unknown',
|
|
837
|
+
declarationKinds,
|
|
838
|
+
sourceSpan: candidateSourceSpan,
|
|
839
|
+
operation: input.operation ?? primaryChange?.operation ?? 'unknown',
|
|
840
|
+
confidence: normalizeSwarmSemanticConfidence(input.confidence ?? primaryChange?.confidence),
|
|
841
|
+
...(conflictReason ? { conflictReason } : {}),
|
|
842
|
+
status: classifySwarmMergeCandidateAdmission({ status: input.status, reasonCodes }),
|
|
843
|
+
reasonCodes,
|
|
844
|
+
semanticChanges,
|
|
845
|
+
changedPaths,
|
|
846
|
+
evidencePaths: uniqueStrings(input.evidencePaths ?? []),
|
|
847
|
+
generatedAt,
|
|
848
|
+
summary: {
|
|
849
|
+
changeCount: semanticChanges.length,
|
|
850
|
+
symbolCount: symbolIds.length,
|
|
851
|
+
reasonCount: reasonCodes.length
|
|
852
|
+
},
|
|
853
|
+
...(metadata ? { metadata } : {})
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
export function mapSwarmMergeCandidatesToGraph(input) {
|
|
857
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
858
|
+
const candidates = input.candidates.map((candidate) => createSwarmMergeCandidate({ ...candidate, generatedAt: candidate.generatedAt ?? generatedAt }));
|
|
859
|
+
const includeChangeNodes = input.includeChangeNodes !== false;
|
|
860
|
+
const nodes = [];
|
|
861
|
+
const edges = [];
|
|
862
|
+
for (const candidate of candidates) {
|
|
863
|
+
const candidateNodeId = graphNodeId('candidate', candidate.id);
|
|
864
|
+
nodes.push({
|
|
865
|
+
id: candidateNodeId,
|
|
866
|
+
kind: 'candidate',
|
|
867
|
+
title: candidate.title ?? candidate.symbolId,
|
|
868
|
+
status: candidate.status,
|
|
869
|
+
jobId: candidate.jobId,
|
|
870
|
+
taskId: candidate.taskId,
|
|
871
|
+
path: candidate.changedPaths[0],
|
|
872
|
+
generatedAt,
|
|
873
|
+
metadata: candidate
|
|
874
|
+
});
|
|
875
|
+
if (!includeChangeNodes)
|
|
876
|
+
continue;
|
|
877
|
+
for (const change of candidate.semanticChanges) {
|
|
878
|
+
const changeNodeId = graphNodeId('semantic-change', change.id);
|
|
879
|
+
nodes.push({
|
|
880
|
+
id: changeNodeId,
|
|
881
|
+
kind: 'semantic-change',
|
|
882
|
+
title: `${change.operation} ${change.symbolId}`,
|
|
883
|
+
status: change.status,
|
|
884
|
+
jobId: candidate.jobId,
|
|
885
|
+
taskId: candidate.taskId,
|
|
886
|
+
path: change.sourceSpan.path,
|
|
887
|
+
generatedAt,
|
|
888
|
+
metadata: change
|
|
889
|
+
});
|
|
890
|
+
edges.push({
|
|
891
|
+
kind: 'produces',
|
|
892
|
+
from: candidateNodeId,
|
|
893
|
+
to: changeNodeId,
|
|
894
|
+
label: 'semantic-change',
|
|
895
|
+
generatedAt,
|
|
896
|
+
metadata: {
|
|
897
|
+
symbolId: change.symbolId,
|
|
898
|
+
declarationKind: change.declarationKind,
|
|
899
|
+
operation: change.operation,
|
|
900
|
+
reasonCodes: change.reasonCodes
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
if (change.conflictReason) {
|
|
904
|
+
edges.push({
|
|
905
|
+
kind: 'conflictsWith',
|
|
906
|
+
from: candidateNodeId,
|
|
907
|
+
to: changeNodeId,
|
|
908
|
+
label: change.conflictReason,
|
|
909
|
+
generatedAt
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return { candidates, nodes, edges };
|
|
915
|
+
}
|
|
916
|
+
export function createSwarmMergeCandidateGraph(input) {
|
|
917
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
918
|
+
const projection = mapSwarmMergeCandidatesToGraph({ ...input, generatedAt });
|
|
919
|
+
return createSwarmRunGraph({
|
|
920
|
+
id: input.id,
|
|
921
|
+
runId: input.runId,
|
|
922
|
+
title: input.title ?? 'Semantic merge candidate graph',
|
|
923
|
+
generatedAt,
|
|
924
|
+
nodes: projection.nodes,
|
|
925
|
+
edges: projection.edges,
|
|
926
|
+
metadata: input.metadata
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
export function mapSwarmReplayRecordsToGraph(input) {
|
|
930
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
931
|
+
const replayRecords = (input.replayRecords ?? []).map((record) => isSwarmReplayRecord(record) ? record : createSwarmReplayRecord(record));
|
|
932
|
+
const nodes = [];
|
|
933
|
+
const edges = [];
|
|
934
|
+
for (const replay of replayRecords) {
|
|
935
|
+
const replayNodeId = graphNodeId('replay', replay.id);
|
|
936
|
+
nodes.push({
|
|
937
|
+
id: replayNodeId,
|
|
938
|
+
kind: 'replay',
|
|
939
|
+
title: replay.title,
|
|
940
|
+
status: replay.status,
|
|
941
|
+
jobId: replay.jobId,
|
|
942
|
+
taskId: replay.taskId,
|
|
943
|
+
generatedAt,
|
|
944
|
+
metadata: replay
|
|
945
|
+
});
|
|
946
|
+
for (const event of replay.patchEvents) {
|
|
947
|
+
const eventNodeId = graphNodeId('replay', event.id);
|
|
948
|
+
nodes.push({
|
|
949
|
+
id: eventNodeId,
|
|
950
|
+
kind: 'replay',
|
|
951
|
+
title: `${event.operation} ${event.path}`,
|
|
952
|
+
jobId: event.jobId ?? replay.jobId,
|
|
953
|
+
taskId: event.taskId ?? replay.taskId,
|
|
954
|
+
path: event.path,
|
|
955
|
+
generatedAt,
|
|
956
|
+
metadata: event
|
|
957
|
+
});
|
|
958
|
+
edges.push({
|
|
959
|
+
kind: 'produces',
|
|
960
|
+
from: replayNodeId,
|
|
961
|
+
to: eventNodeId,
|
|
962
|
+
label: event.operation,
|
|
963
|
+
generatedAt
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
for (const evidence of replay.evidenceRefs) {
|
|
967
|
+
const evidenceNodeId = graphNodeId('evidence', evidence.id);
|
|
968
|
+
nodes.push({
|
|
969
|
+
id: evidenceNodeId,
|
|
970
|
+
kind: 'evidence',
|
|
971
|
+
title: evidence.path ?? evidence.uri ?? evidence.id,
|
|
972
|
+
path: evidence.path,
|
|
973
|
+
generatedAt,
|
|
974
|
+
metadata: evidence
|
|
975
|
+
});
|
|
976
|
+
edges.push({
|
|
977
|
+
kind: 'verifies',
|
|
978
|
+
from: evidenceNodeId,
|
|
979
|
+
to: replayNodeId,
|
|
980
|
+
label: evidence.role ?? evidence.kind,
|
|
981
|
+
generatedAt
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return createSwarmRunGraph({
|
|
986
|
+
runId: input.runId,
|
|
987
|
+
title: 'Replay records graph',
|
|
988
|
+
generatedAt,
|
|
989
|
+
nodes,
|
|
990
|
+
edges
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
export function createSwarmReplayRecordGraph(input) {
|
|
994
|
+
return mapSwarmReplayRecordsToGraph(input);
|
|
995
|
+
}
|
|
996
|
+
export function mapSwarmImprovementLoopsToGraph(input) {
|
|
997
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
998
|
+
const loops = (input.improvementLoops ?? []).map((loop) => isSwarmImprovementLoop(loop) ? loop : createSwarmImprovementLoop(loop));
|
|
999
|
+
const nodes = [];
|
|
1000
|
+
const edges = [];
|
|
1001
|
+
for (const loop of loops) {
|
|
1002
|
+
const phaseNodes = [];
|
|
1003
|
+
if (loop.observation !== undefined) {
|
|
1004
|
+
phaseNodes.push(createImprovementLoopPhaseNode(loop, 'observation', loop.observation, generatedAt));
|
|
1005
|
+
}
|
|
1006
|
+
if (loop.action !== undefined) {
|
|
1007
|
+
phaseNodes.push(createImprovementLoopPhaseNode(loop, 'action', loop.action, generatedAt));
|
|
1008
|
+
}
|
|
1009
|
+
if (loop.result !== undefined) {
|
|
1010
|
+
phaseNodes.push(createImprovementLoopPhaseNode(loop, 'result', loop.result, generatedAt));
|
|
1011
|
+
}
|
|
1012
|
+
if (phaseNodes.length === 0) {
|
|
1013
|
+
phaseNodes.push({
|
|
1014
|
+
id: graphNodeId('rsi', loop.id),
|
|
1015
|
+
kind: 'rsi',
|
|
1016
|
+
title: loop.title,
|
|
1017
|
+
status: loop.status,
|
|
1018
|
+
jobId: loop.jobId,
|
|
1019
|
+
taskId: loop.taskId,
|
|
1020
|
+
generatedAt,
|
|
1021
|
+
metadata: loop
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
nodes.push(...phaseNodes);
|
|
1025
|
+
for (let index = 1; index < phaseNodes.length; index += 1) {
|
|
1026
|
+
edges.push({
|
|
1027
|
+
kind: 'produces',
|
|
1028
|
+
from: phaseNodes[index - 1].id,
|
|
1029
|
+
to: phaseNodes[index].id,
|
|
1030
|
+
label: index === 1 ? 'action' : 'result',
|
|
1031
|
+
generatedAt
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
const entryNodeId = phaseNodes[0]?.id;
|
|
1035
|
+
const exitNodeId = phaseNodes.at(-1)?.id;
|
|
1036
|
+
for (const replayRecordId of loop.replayRecordIds) {
|
|
1037
|
+
const replayNodeId = graphNodeId('replay', replayRecordId);
|
|
1038
|
+
nodes.push({
|
|
1039
|
+
id: replayNodeId,
|
|
1040
|
+
kind: 'replay',
|
|
1041
|
+
title: titleFromId(replayRecordId),
|
|
1042
|
+
generatedAt,
|
|
1043
|
+
metadata: { replayRecordId }
|
|
1044
|
+
});
|
|
1045
|
+
if (entryNodeId)
|
|
1046
|
+
edges.push({ kind: 'produces', from: replayNodeId, to: entryNodeId, label: 'feeds-rsi', generatedAt });
|
|
1047
|
+
}
|
|
1048
|
+
for (const patchEventId of loop.patchEventIds) {
|
|
1049
|
+
const patchNodeId = graphNodeId('replay', patchEventId);
|
|
1050
|
+
nodes.push({
|
|
1051
|
+
id: patchNodeId,
|
|
1052
|
+
kind: 'replay',
|
|
1053
|
+
title: titleFromId(patchEventId),
|
|
1054
|
+
generatedAt,
|
|
1055
|
+
metadata: { patchEventId }
|
|
1056
|
+
});
|
|
1057
|
+
if (entryNodeId)
|
|
1058
|
+
edges.push({ kind: 'produces', from: patchNodeId, to: entryNodeId, label: 'feeds-rsi', generatedAt });
|
|
1059
|
+
}
|
|
1060
|
+
for (const evidence of loop.evidenceRefs) {
|
|
1061
|
+
const evidenceNodeId = graphNodeId('evidence', evidence.id);
|
|
1062
|
+
nodes.push({
|
|
1063
|
+
id: evidenceNodeId,
|
|
1064
|
+
kind: 'evidence',
|
|
1065
|
+
title: evidence.path ?? evidence.uri ?? evidence.id,
|
|
1066
|
+
path: evidence.path,
|
|
1067
|
+
generatedAt,
|
|
1068
|
+
metadata: evidence
|
|
1069
|
+
});
|
|
1070
|
+
if (exitNodeId)
|
|
1071
|
+
edges.push({ kind: 'verifies', from: evidenceNodeId, to: exitNodeId, label: evidence.role ?? evidence.kind, generatedAt });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return createSwarmRunGraph({
|
|
1075
|
+
runId: input.runId,
|
|
1076
|
+
title: 'Improvement loop graph',
|
|
1077
|
+
generatedAt,
|
|
1078
|
+
nodes,
|
|
1079
|
+
edges
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
export function createSwarmImprovementLoopGraph(input) {
|
|
1083
|
+
return mapSwarmImprovementLoopsToGraph(input);
|
|
1084
|
+
}
|
|
1085
|
+
export function createSwarmRunGraphChunk(input = {}) {
|
|
1086
|
+
const generatedAt = Date.now();
|
|
1087
|
+
const nodes = dedupeGraphNodes((input.nodes ?? []).map((node) => normalizeGraphNode(node, generatedAt)));
|
|
1088
|
+
const known = new Set(nodes.map((node) => node.id));
|
|
1089
|
+
const edges = dedupeGraphEdges((input.edges ?? [])
|
|
1090
|
+
.map((edge) => normalizeGraphEdge(edge, generatedAt))
|
|
1091
|
+
.filter((edge) => known.has(edge.from) && known.has(edge.to)));
|
|
1092
|
+
const entryNodeIds = uniqueStrings((input.entryNodeIds ?? []).filter((id) => known.has(id)));
|
|
1093
|
+
const exitNodeIds = uniqueStrings((input.exitNodeIds ?? []).filter((id) => known.has(id)));
|
|
1094
|
+
return {
|
|
1095
|
+
kind: FRONTIER_SWARM_RUN_GRAPH_CHUNK_KIND,
|
|
1096
|
+
version: FRONTIER_SWARM_RUN_GRAPH_CHUNK_VERSION,
|
|
1097
|
+
id: input.id ?? 'swarm-run-graph-chunk:' + stableHash([nodes.map((node) => node.id), edges.map((edge) => edge.id), entryNodeIds, exitNodeIds]),
|
|
1098
|
+
nodes,
|
|
1099
|
+
edges,
|
|
1100
|
+
entryNodeIds,
|
|
1101
|
+
exitNodeIds,
|
|
1102
|
+
summary: {
|
|
1103
|
+
nodeCount: nodes.length,
|
|
1104
|
+
edgeCount: edges.length,
|
|
1105
|
+
entryCount: entryNodeIds.length,
|
|
1106
|
+
exitCount: exitNodeIds.length
|
|
1107
|
+
},
|
|
1108
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
export function createSwarmRunGraphChain(input) {
|
|
1112
|
+
const nodes = input.nodes.map((node) => normalizeGraphNode(node));
|
|
1113
|
+
const edges = [];
|
|
1114
|
+
for (let index = 1; index < nodes.length; index += 1) {
|
|
1115
|
+
edges.push({ kind: input.edgeKind ?? 'dependsOn', from: nodes[index - 1].id, to: nodes[index].id });
|
|
1116
|
+
}
|
|
1117
|
+
return createSwarmRunGraphChunk({
|
|
1118
|
+
id: input.id,
|
|
1119
|
+
nodes,
|
|
1120
|
+
edges,
|
|
1121
|
+
entryNodeIds: nodes[0] ? [nodes[0].id] : [],
|
|
1122
|
+
exitNodeIds: nodes.at(-1) ? [nodes.at(-1).id] : [],
|
|
1123
|
+
metadata: input.metadata
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
export function createSwarmRunGraphFork(input) {
|
|
1127
|
+
const source = normalizeGraphNode(input.source);
|
|
1128
|
+
const branches = input.branches.map((node) => normalizeGraphNode(node));
|
|
1129
|
+
return createSwarmRunGraphChunk({
|
|
1130
|
+
id: input.id,
|
|
1131
|
+
nodes: [source, ...branches],
|
|
1132
|
+
edges: branches.map((branch) => ({ kind: 'dependsOn', from: source.id, to: branch.id, label: 'fork' })),
|
|
1133
|
+
entryNodeIds: [source.id],
|
|
1134
|
+
exitNodeIds: branches.map((branch) => branch.id),
|
|
1135
|
+
metadata: input.metadata
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
export function createSwarmRunGraphJoin(input) {
|
|
1139
|
+
const branches = input.branches.map((node) => normalizeGraphNode(node));
|
|
1140
|
+
const join = normalizeGraphNode(input.join);
|
|
1141
|
+
return createSwarmRunGraphChunk({
|
|
1142
|
+
id: input.id,
|
|
1143
|
+
nodes: [...branches, join],
|
|
1144
|
+
edges: branches.map((branch) => ({ kind: 'dependsOn', from: branch.id, to: join.id, label: 'join' })),
|
|
1145
|
+
entryNodeIds: branches.map((branch) => branch.id),
|
|
1146
|
+
exitNodeIds: [join.id],
|
|
1147
|
+
metadata: input.metadata
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
export function createSwarmRunGraphBarrier(input) {
|
|
1151
|
+
const prerequisites = input.prerequisites.map((node) => normalizeGraphNode(node));
|
|
1152
|
+
const prerequisiteIds = prerequisites.map((node) => node.id);
|
|
1153
|
+
const barrierMetadata = toJsonObject(input.barrier?.metadata);
|
|
1154
|
+
const barrier = normalizeGraphNode({
|
|
1155
|
+
...(input.barrier ?? {}),
|
|
1156
|
+
id: input.barrier?.id ?? (input.id ? `${input.id}:barrier` : graphNodeId('gate', 'barrier:' + stableHash(prerequisiteIds))),
|
|
1157
|
+
kind: input.barrier?.kind ?? 'gate',
|
|
1158
|
+
title: input.barrier?.title ?? 'Barrier',
|
|
1159
|
+
metadata: {
|
|
1160
|
+
...(barrierMetadata ?? {}),
|
|
1161
|
+
role: 'barrier',
|
|
1162
|
+
graphChunkKind: 'barrier',
|
|
1163
|
+
prerequisiteIds,
|
|
1164
|
+
prerequisiteCount: prerequisiteIds.length
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
return createSwarmRunGraphChunk({
|
|
1168
|
+
id: input.id,
|
|
1169
|
+
nodes: [...prerequisites, barrier],
|
|
1170
|
+
edges: prerequisites.map((node, index) => ({
|
|
1171
|
+
kind: 'dependsOn',
|
|
1172
|
+
from: node.id,
|
|
1173
|
+
to: barrier.id,
|
|
1174
|
+
label: 'barrier',
|
|
1175
|
+
metadata: {
|
|
1176
|
+
role: 'barrier-prerequisite',
|
|
1177
|
+
barrierId: barrier.id,
|
|
1178
|
+
prerequisiteId: node.id,
|
|
1179
|
+
prerequisiteIndex: index,
|
|
1180
|
+
prerequisiteCount: prerequisites.length
|
|
1181
|
+
}
|
|
1182
|
+
})),
|
|
1183
|
+
entryNodeIds: prerequisiteIds,
|
|
1184
|
+
exitNodeIds: [barrier.id],
|
|
1185
|
+
metadata: input.metadata
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
export function createSwarmRunGraphRaceSelect(input) {
|
|
1189
|
+
const baseBranches = input.branches.map((node) => normalizeGraphNode(node));
|
|
1190
|
+
const branchIds = baseBranches.map((node) => node.id);
|
|
1191
|
+
const selectedBranchId = input.selectedBranchId && branchIds.includes(input.selectedBranchId) ? input.selectedBranchId : undefined;
|
|
1192
|
+
const rejectedBranchIds = new Set(selectedBranchId
|
|
1193
|
+
? branchIds.filter((id) => id !== selectedBranchId)
|
|
1194
|
+
: uniqueStrings(input.rejectedBranchIds ?? []).filter((id) => branchIds.includes(id)));
|
|
1195
|
+
if (selectedBranchId)
|
|
1196
|
+
rejectedBranchIds.delete(selectedBranchId);
|
|
1197
|
+
const selectorMetadata = toJsonObject(input.selector?.metadata);
|
|
1198
|
+
const selector = normalizeGraphNode({
|
|
1199
|
+
...(input.selector ?? {}),
|
|
1200
|
+
id: input.selector?.id ?? (input.id ? `${input.id}:selector` : graphNodeId('decision', 'race-select:' + stableHash(branchIds))),
|
|
1201
|
+
kind: input.selector?.kind ?? 'decision',
|
|
1202
|
+
title: input.selector?.title ?? 'Race/select',
|
|
1203
|
+
metadata: {
|
|
1204
|
+
...(selectorMetadata ?? {}),
|
|
1205
|
+
role: 'race-select',
|
|
1206
|
+
graphChunkKind: 'race-select',
|
|
1207
|
+
branchIds,
|
|
1208
|
+
branchCount: branchIds.length,
|
|
1209
|
+
...(selectedBranchId ? { selectedBranchId } : {}),
|
|
1210
|
+
rejectedBranchIds: Array.from(rejectedBranchIds).sort()
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
const branches = baseBranches.map((node, index) => {
|
|
1214
|
+
const disposition = node.id === selectedBranchId ? 'selected' : rejectedBranchIds.has(node.id) ? 'rejected' : 'candidate';
|
|
1215
|
+
return normalizeGraphNode({
|
|
1216
|
+
...node,
|
|
1217
|
+
status: node.status ?? (disposition === 'selected' ? 'accepted' : disposition === 'rejected' ? 'rejected' : undefined),
|
|
1218
|
+
metadata: {
|
|
1219
|
+
...(node.metadata ?? {}),
|
|
1220
|
+
raceSelect: {
|
|
1221
|
+
selectorId: selector.id,
|
|
1222
|
+
branchId: node.id,
|
|
1223
|
+
branchIndex: index,
|
|
1224
|
+
branchCount: baseBranches.length,
|
|
1225
|
+
disposition,
|
|
1226
|
+
selected: disposition === 'selected',
|
|
1227
|
+
rejected: disposition === 'rejected'
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}, node.generatedAt);
|
|
1231
|
+
});
|
|
1232
|
+
return createSwarmRunGraphChunk({
|
|
1233
|
+
id: input.id,
|
|
1234
|
+
nodes: [selector, ...branches],
|
|
1235
|
+
edges: branches.map((branch, index) => {
|
|
1236
|
+
const disposition = branch.id === selectedBranchId ? 'selected' : rejectedBranchIds.has(branch.id) ? 'rejected' : 'candidate';
|
|
1237
|
+
return {
|
|
1238
|
+
kind: disposition === 'selected' ? 'mergesInto' : disposition === 'rejected' ? 'supersedes' : 'dependsOn',
|
|
1239
|
+
from: branch.id,
|
|
1240
|
+
to: selector.id,
|
|
1241
|
+
label: disposition,
|
|
1242
|
+
metadata: {
|
|
1243
|
+
role: 'race-select-branch',
|
|
1244
|
+
selectorId: selector.id,
|
|
1245
|
+
branchId: branch.id,
|
|
1246
|
+
branchIndex: index,
|
|
1247
|
+
branchCount: branches.length,
|
|
1248
|
+
disposition,
|
|
1249
|
+
selected: disposition === 'selected',
|
|
1250
|
+
rejected: disposition === 'rejected'
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
}),
|
|
1254
|
+
entryNodeIds: branchIds,
|
|
1255
|
+
exitNodeIds: [selector.id],
|
|
1256
|
+
metadata: input.metadata
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
export function createSwarmRunGraphTournament(input) {
|
|
1260
|
+
const tournament = normalizeGraphNode({ id: input.id ? `${input.id}:tournament` : undefined, kind: 'decision', title: 'Tournament', metadata: input.metadata });
|
|
1261
|
+
const candidates = input.candidates.map((node) => normalizeGraphNode({ ...node, kind: node.kind ?? 'candidate' }));
|
|
1262
|
+
const rejected = new Set(input.rejectedCandidates ?? []);
|
|
1263
|
+
const edges = candidates.map((candidate) => ({
|
|
1264
|
+
kind: candidate.id === input.winner ? 'mergesInto' : rejected.has(candidate.id) ? 'supersedes' : 'dependsOn',
|
|
1265
|
+
from: candidate.id,
|
|
1266
|
+
to: tournament.id,
|
|
1267
|
+
label: candidate.id === input.winner ? 'winner' : rejected.has(candidate.id) ? 'rejected' : 'candidate'
|
|
1268
|
+
}));
|
|
1269
|
+
return createSwarmRunGraphChunk({
|
|
1270
|
+
id: input.id,
|
|
1271
|
+
nodes: [tournament, ...candidates],
|
|
1272
|
+
edges,
|
|
1273
|
+
entryNodeIds: candidates.map((candidate) => candidate.id),
|
|
1274
|
+
exitNodeIds: [tournament.id],
|
|
1275
|
+
metadata: input.metadata
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
export function createSwarmRunGraphRetryLoop(input) {
|
|
1279
|
+
const action = normalizeGraphNode(input.action);
|
|
1280
|
+
const gate = normalizeGraphNode({ ...input.gate, kind: input.gate.kind ?? 'gate' });
|
|
1281
|
+
const retry = normalizeGraphNode(input.retry);
|
|
1282
|
+
const optional = [input.success, input.failure].filter((node) => Boolean(node)).map((node) => normalizeGraphNode(node));
|
|
1283
|
+
return createSwarmRunGraphChunk({
|
|
1284
|
+
id: input.id,
|
|
1285
|
+
nodes: [action, gate, retry, ...optional],
|
|
1286
|
+
edges: [
|
|
1287
|
+
{ kind: 'verifies', from: action.id, to: gate.id },
|
|
1288
|
+
{ kind: 'blocks', from: gate.id, to: retry.id, label: 'retry' },
|
|
1289
|
+
{ kind: 'dependsOn', from: retry.id, to: action.id, label: 'loop' },
|
|
1290
|
+
...optional.map((node) => ({ kind: node.status === 'failed' ? 'blocks' : 'mergesInto', from: gate.id, to: node.id }))
|
|
1291
|
+
],
|
|
1292
|
+
entryNodeIds: [action.id],
|
|
1293
|
+
exitNodeIds: optional.length > 0 ? optional.map((node) => node.id) : [gate.id],
|
|
1294
|
+
metadata: input.metadata
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
export function createSwarmRunGraphRsiLoop(input) {
|
|
1298
|
+
const observe = normalizeGraphNode({ ...input.observe, kind: input.observe.kind ?? 'rsi' });
|
|
1299
|
+
const improve = normalizeGraphNode({ ...input.improve, kind: input.improve.kind ?? 'rsi' });
|
|
1300
|
+
const apply = input.apply ? normalizeGraphNode(input.apply) : undefined;
|
|
1301
|
+
return createSwarmRunGraphChunk({
|
|
1302
|
+
id: input.id,
|
|
1303
|
+
nodes: apply ? [observe, improve, apply] : [observe, improve],
|
|
1304
|
+
edges: [
|
|
1305
|
+
{ kind: 'produces', from: observe.id, to: improve.id, label: 'feedback' },
|
|
1306
|
+
...(apply ? [{ kind: 'mergesInto', from: improve.id, to: apply.id, label: 'applies' }] : [])
|
|
1307
|
+
],
|
|
1308
|
+
entryNodeIds: [observe.id],
|
|
1309
|
+
exitNodeIds: [apply?.id ?? improve.id],
|
|
1310
|
+
metadata: input.metadata
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
export function createSwarmRunGraphSynthesisChunk(input = {}) {
|
|
1314
|
+
const source = input.source ? normalizeGraphNode(input.source) : undefined;
|
|
1315
|
+
const panel = normalizeGraphNode({
|
|
1316
|
+
...input.panel,
|
|
1317
|
+
id: input.panel?.id ?? (input.id ? `${input.id}:panel` : undefined),
|
|
1318
|
+
kind: input.panel?.kind ?? 'decision',
|
|
1319
|
+
title: input.panel?.title ?? 'Synthesis panel'
|
|
1320
|
+
});
|
|
1321
|
+
const candidates = (input.candidates ?? []).map((candidate) => normalizeGraphNode({
|
|
1322
|
+
...candidate,
|
|
1323
|
+
kind: candidate.kind ?? 'candidate'
|
|
1324
|
+
}));
|
|
1325
|
+
const rejectedCandidateIds = new Set(input.rejectedCandidateIds ?? candidates
|
|
1326
|
+
.filter((candidate) => candidate.status === 'rejected' || candidate.status === 'failed' || candidate.status === 'superseded')
|
|
1327
|
+
.map((candidate) => candidate.id));
|
|
1328
|
+
const selectedCandidateId = input.selectedCandidateId ?? candidates.find((candidate) => !rejectedCandidateIds.has(candidate.id))?.id;
|
|
1329
|
+
const selectedCandidate = selectedCandidateId ? candidates.find((candidate) => candidate.id === selectedCandidateId) : undefined;
|
|
1330
|
+
const decision = input.decision ? normalizeGraphNode({
|
|
1331
|
+
...input.decision,
|
|
1332
|
+
id: input.decision.id ?? (input.id ? `${input.id}:decision` : undefined),
|
|
1333
|
+
kind: input.decision.kind ?? 'decision',
|
|
1334
|
+
title: input.decision.title ?? 'Coordinator synthesis decision'
|
|
1335
|
+
}) : undefined;
|
|
1336
|
+
const output = input.output ? normalizeGraphNode({
|
|
1337
|
+
...input.output,
|
|
1338
|
+
kind: input.output.kind ?? 'candidate'
|
|
1339
|
+
}) : undefined;
|
|
1340
|
+
const edges = [];
|
|
1341
|
+
if (source) {
|
|
1342
|
+
edges.push({ kind: 'produces', from: source.id, to: panel.id, label: 'synthesis-request' });
|
|
1343
|
+
}
|
|
1344
|
+
for (const candidate of candidates) {
|
|
1345
|
+
edges.push({ kind: 'produces', from: panel.id, to: candidate.id, label: 'candidate' });
|
|
1346
|
+
}
|
|
1347
|
+
if (decision && selectedCandidate) {
|
|
1348
|
+
edges.push({ kind: 'mergesInto', from: selectedCandidate.id, to: decision.id, label: 'selected' });
|
|
1349
|
+
}
|
|
1350
|
+
for (const candidate of candidates) {
|
|
1351
|
+
if (!rejectedCandidateIds.has(candidate.id))
|
|
1352
|
+
continue;
|
|
1353
|
+
edges.push(selectedCandidate && selectedCandidate.id !== candidate.id
|
|
1354
|
+
? { kind: 'supersedes', from: selectedCandidate.id, to: candidate.id, label: 'rejected' }
|
|
1355
|
+
: { kind: 'supersedes', from: candidate.id, to: decision?.id ?? panel.id, label: 'rejected' });
|
|
1356
|
+
}
|
|
1357
|
+
if (decision && output) {
|
|
1358
|
+
edges.push({
|
|
1359
|
+
kind: isBlockingGraphStatus(decision.status) ? 'blocks' : 'produces',
|
|
1360
|
+
from: decision.id,
|
|
1361
|
+
to: output.id,
|
|
1362
|
+
label: isBlockingGraphStatus(decision.status) ? 'blocked-output' : 'synthesized-output'
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
else if (selectedCandidate && output) {
|
|
1366
|
+
edges.push({ kind: 'mergesInto', from: selectedCandidate.id, to: output.id, label: 'synthesized-output' });
|
|
1367
|
+
}
|
|
1368
|
+
const nodes = [source, panel, ...candidates, decision, output].filter((node) => Boolean(node));
|
|
1369
|
+
return createSwarmRunGraphChunk({
|
|
1370
|
+
id: input.id,
|
|
1371
|
+
nodes,
|
|
1372
|
+
edges,
|
|
1373
|
+
entryNodeIds: [source?.id ?? panel.id],
|
|
1374
|
+
exitNodeIds: [output?.id ?? decision?.id ?? selectedCandidate?.id ?? panel.id],
|
|
1375
|
+
metadata: input.metadata
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
export function createSwarmRunGraphVerificationGateChunk(input) {
|
|
1379
|
+
const subject = normalizeGraphNode(input.subject);
|
|
1380
|
+
const gate = normalizeGraphNode({
|
|
1381
|
+
...input.gate,
|
|
1382
|
+
kind: input.gate.kind ?? 'gate',
|
|
1383
|
+
title: input.gate.title ?? 'Verification gate'
|
|
1384
|
+
});
|
|
1385
|
+
const evidence = (input.evidence ?? []).map((node) => normalizeGraphNode({
|
|
1386
|
+
...node,
|
|
1387
|
+
kind: node.kind ?? 'evidence'
|
|
1388
|
+
}));
|
|
1389
|
+
const pass = input.pass ? normalizeGraphNode(input.pass) : undefined;
|
|
1390
|
+
const block = input.block ? normalizeGraphNode(input.block) : undefined;
|
|
1391
|
+
const passing = isPassingGraphStatus(gate.status);
|
|
1392
|
+
const blocking = isBlockingGraphStatus(gate.status);
|
|
1393
|
+
const edges = [
|
|
1394
|
+
{ kind: 'verifies', from: gate.id, to: subject.id, label: 'gate' },
|
|
1395
|
+
...evidence.map((node) => ({ kind: 'produces', from: gate.id, to: node.id, label: 'evidence' })),
|
|
1396
|
+
...evidence.map((node) => ({ kind: 'verifies', from: node.id, to: gate.id, label: 'evidence-verifies-gate' }))
|
|
1397
|
+
];
|
|
1398
|
+
if (passing && pass) {
|
|
1399
|
+
edges.push({ kind: 'mergesInto', from: gate.id, to: pass.id, label: 'passed' });
|
|
1400
|
+
}
|
|
1401
|
+
if (blocking) {
|
|
1402
|
+
edges.push({ kind: 'blocks', from: gate.id, to: block?.id ?? subject.id, label: gate.status === 'failed' ? 'failed' : 'blocked' });
|
|
1403
|
+
}
|
|
1404
|
+
return createSwarmRunGraphChunk({
|
|
1405
|
+
id: input.id,
|
|
1406
|
+
nodes: [subject, gate, ...evidence, pass, block].filter((node) => Boolean(node)),
|
|
1407
|
+
edges,
|
|
1408
|
+
entryNodeIds: [subject.id],
|
|
1409
|
+
exitNodeIds: [passing && pass ? pass.id : blocking && block ? block.id : gate.id],
|
|
1410
|
+
metadata: input.metadata
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
export function createSwarmRunGraphMergeGateChunk(input) {
|
|
1414
|
+
const candidate = normalizeGraphNode({
|
|
1415
|
+
...input.candidate,
|
|
1416
|
+
kind: input.candidate.kind ?? 'candidate'
|
|
1417
|
+
});
|
|
1418
|
+
const target = normalizeGraphNode(input.target);
|
|
1419
|
+
const gate = input.gate ? normalizeGraphNode({
|
|
1420
|
+
...input.gate,
|
|
1421
|
+
kind: input.gate.kind ?? 'gate',
|
|
1422
|
+
title: input.gate.title ?? 'Merge gate'
|
|
1423
|
+
}) : undefined;
|
|
1424
|
+
const decision = input.decision ? normalizeGraphNode({
|
|
1425
|
+
...input.decision,
|
|
1426
|
+
kind: input.decision.kind ?? 'decision',
|
|
1427
|
+
title: input.decision.title ?? 'Coordinator merge decision'
|
|
1428
|
+
}) : undefined;
|
|
1429
|
+
const blockers = (input.blockers ?? []).map((node) => normalizeGraphNode({
|
|
1430
|
+
...node,
|
|
1431
|
+
kind: node.kind ?? 'gate'
|
|
1432
|
+
}));
|
|
1433
|
+
const superseded = (input.superseded ?? []).map((node) => normalizeGraphNode({
|
|
1434
|
+
...node,
|
|
1435
|
+
kind: node.kind ?? 'candidate'
|
|
1436
|
+
}));
|
|
1437
|
+
const status = input.status ?? decision?.status ?? gate?.status ?? candidate.status;
|
|
1438
|
+
const passing = isPassingGraphStatus(status);
|
|
1439
|
+
const blocking = isBlockingGraphStatus(status);
|
|
1440
|
+
const edges = [];
|
|
1441
|
+
if (gate) {
|
|
1442
|
+
edges.push({ kind: 'verifies', from: gate.id, to: candidate.id, label: 'merge-gate' });
|
|
1443
|
+
}
|
|
1444
|
+
if (gate && decision) {
|
|
1445
|
+
edges.push({ kind: blocking ? 'blocks' : 'produces', from: gate.id, to: decision.id, label: 'gate-decision' });
|
|
1446
|
+
}
|
|
1447
|
+
if (decision) {
|
|
1448
|
+
edges.push({ kind: 'verifies', from: decision.id, to: candidate.id, label: 'coordinator-decision' });
|
|
1449
|
+
}
|
|
1450
|
+
for (const blocker of blockers) {
|
|
1451
|
+
edges.push({ kind: 'blocks', from: blocker.id, to: candidate.id, label: 'merge-blocker' });
|
|
1452
|
+
}
|
|
1453
|
+
for (const node of superseded) {
|
|
1454
|
+
edges.push({ kind: 'supersedes', from: candidate.id, to: node.id, label: 'superseded' });
|
|
1455
|
+
}
|
|
1456
|
+
if (passing) {
|
|
1457
|
+
edges.push({ kind: 'mergesInto', from: candidate.id, to: target.id, label: 'accepted' });
|
|
1458
|
+
}
|
|
1459
|
+
else if (blocking) {
|
|
1460
|
+
edges.push({ kind: 'blocks', from: decision?.id ?? gate?.id ?? candidate.id, to: target.id, label: status === 'failed' ? 'failed' : 'blocked' });
|
|
1461
|
+
}
|
|
1462
|
+
return createSwarmRunGraphChunk({
|
|
1463
|
+
id: input.id,
|
|
1464
|
+
nodes: [candidate, target, gate, decision, ...blockers, ...superseded].filter((node) => Boolean(node)),
|
|
1465
|
+
edges,
|
|
1466
|
+
entryNodeIds: [candidate.id],
|
|
1467
|
+
exitNodeIds: [passing ? target.id : blocking ? decision?.id ?? gate?.id ?? candidate.id : target.id],
|
|
1468
|
+
metadata: input.metadata
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
381
1471
|
export const FRONTIER_SWARM_TASK_MODEL_PROFILES = [
|
|
382
1472
|
{
|
|
383
1473
|
workKind: 'agent-task',
|
|
@@ -750,6 +1840,497 @@ export function createSwarmRun(input) {
|
|
|
750
1840
|
};
|
|
751
1841
|
return run;
|
|
752
1842
|
}
|
|
1843
|
+
export function createRunEventsFromSwarmPlan(plan, options = {}) {
|
|
1844
|
+
const actorId = options.actorId ?? 'frontier-swarm';
|
|
1845
|
+
const runId = options.runId ?? plan.runId;
|
|
1846
|
+
const time = swarmRunEventTime(options, plan.createdAt);
|
|
1847
|
+
let actorSeq = options.startActorSeq ?? 1;
|
|
1848
|
+
const events = [];
|
|
1849
|
+
const created = createRunEvent({
|
|
1850
|
+
runId,
|
|
1851
|
+
actorId,
|
|
1852
|
+
actorSeq: actorSeq++,
|
|
1853
|
+
parents: [...(options.parents ?? [])],
|
|
1854
|
+
time,
|
|
1855
|
+
type: 'run.created',
|
|
1856
|
+
payload: {
|
|
1857
|
+
goal: plan.metadata?.objective ?? plan.metadata?.goal ?? `Frontier swarm ${plan.id}`,
|
|
1858
|
+
metadata: pruneUndefinedJsonObject({
|
|
1859
|
+
source: 'frontier-swarm.plan',
|
|
1860
|
+
planId: plan.id,
|
|
1861
|
+
manifestId: plan.manifestId,
|
|
1862
|
+
summary: plan.summary,
|
|
1863
|
+
filters: plan.filters,
|
|
1864
|
+
limits: plan.limits,
|
|
1865
|
+
metadata: plan.metadata,
|
|
1866
|
+
adapterMetadata: toJsonObject(options.metadata)
|
|
1867
|
+
})
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
events.push(created);
|
|
1871
|
+
const laneIds = uniqueStrings(plan.jobs.map((job) => job.lane));
|
|
1872
|
+
const laneEventByLane = new Map();
|
|
1873
|
+
for (const lane of laneIds) {
|
|
1874
|
+
const laneJobs = plan.jobs.filter((job) => job.lane === lane);
|
|
1875
|
+
const laneEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunLane({
|
|
1876
|
+
id: swarmRunLaneNodeId(lane),
|
|
1877
|
+
title: lane,
|
|
1878
|
+
packageId: inferSwarmLanePackageId(laneJobs),
|
|
1879
|
+
allowedWrites: uniqueStrings(laneJobs.flatMap((job) => job.allowedWrites)),
|
|
1880
|
+
requiredChecks: uniqueStrings(laneJobs.flatMap((job) => job.verification.map(formatSwarmCommandLine))),
|
|
1881
|
+
maxConcurrency: plan.limits.maxLaneConcurrency[lane],
|
|
1882
|
+
concurrencyKey: lane,
|
|
1883
|
+
semanticRegions: uniqueStrings(laneJobs.flatMap((job) => [...job.ownedRegions, ...job.changedRegions])),
|
|
1884
|
+
status: 'ready',
|
|
1885
|
+
createdAt: time,
|
|
1886
|
+
updatedAt: time,
|
|
1887
|
+
metadata: pruneUndefinedJsonObject({
|
|
1888
|
+
source: 'frontier-swarm.plan.lane',
|
|
1889
|
+
jobIds: laneJobs.map((job) => job.id),
|
|
1890
|
+
taskIds: uniqueStrings(laneJobs.map((job) => job.taskId))
|
|
1891
|
+
})
|
|
1892
|
+
}), {
|
|
1893
|
+
parents: [created.id],
|
|
1894
|
+
time
|
|
1895
|
+
});
|
|
1896
|
+
events.push(laneEvent);
|
|
1897
|
+
laneEventByLane.set(lane, laneEvent);
|
|
1898
|
+
}
|
|
1899
|
+
const taskById = new Map();
|
|
1900
|
+
for (const job of plan.jobs)
|
|
1901
|
+
if (!taskById.has(job.taskId))
|
|
1902
|
+
taskById.set(job.taskId, job);
|
|
1903
|
+
for (const job of taskById.values()) {
|
|
1904
|
+
const task = job.task;
|
|
1905
|
+
const taskEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunTask({
|
|
1906
|
+
id: swarmRunTaskNodeId(job.taskId),
|
|
1907
|
+
title: task.title || job.title,
|
|
1908
|
+
laneId: swarmRunLaneNodeId(job.lane),
|
|
1909
|
+
status: swarmStatusToRunStatus(job.status),
|
|
1910
|
+
targetRefs: uniqueStrings([...(task.targetRefs ?? []), ...job.allowedWrites]),
|
|
1911
|
+
sourceRefs: task.sourceRefs,
|
|
1912
|
+
allowedWrites: job.allowedWrites,
|
|
1913
|
+
semanticRegions: uniqueStrings([...job.ownedRegions, ...job.changedRegions]),
|
|
1914
|
+
acceptance: job.acceptance,
|
|
1915
|
+
verification: job.verification.map((command) => ({
|
|
1916
|
+
id: command.name,
|
|
1917
|
+
command: command.command,
|
|
1918
|
+
args: command.args,
|
|
1919
|
+
cwd: command.cwd,
|
|
1920
|
+
required: command.required,
|
|
1921
|
+
metadata: command.metadata
|
|
1922
|
+
})),
|
|
1923
|
+
priority: job.priority,
|
|
1924
|
+
concurrencyKey: job.concurrencyKey,
|
|
1925
|
+
createdAt: time,
|
|
1926
|
+
updatedAt: time,
|
|
1927
|
+
metadata: pruneUndefinedJsonObject({
|
|
1928
|
+
source: 'frontier-swarm.plan.task',
|
|
1929
|
+
jobIds: plan.jobs.filter((entry) => entry.taskId === job.taskId).map((entry) => entry.id),
|
|
1930
|
+
workKind: task.workKind,
|
|
1931
|
+
layer: job.layer,
|
|
1932
|
+
compute: job.compute.id,
|
|
1933
|
+
tags: job.tags,
|
|
1934
|
+
task: task
|
|
1935
|
+
})
|
|
1936
|
+
}), {
|
|
1937
|
+
parents: [laneEventByLane.get(job.lane)?.id ?? created.id],
|
|
1938
|
+
time
|
|
1939
|
+
});
|
|
1940
|
+
events.push(taskEvent);
|
|
1941
|
+
}
|
|
1942
|
+
return events;
|
|
1943
|
+
}
|
|
1944
|
+
export function createRunEventsFromSwarmLease(lease, options = {}) {
|
|
1945
|
+
const actorId = options.actorId ?? lease.workerId ?? 'frontier-swarm';
|
|
1946
|
+
const runId = options.runId ?? options.job?.metadata?.runId ?? 'frontier-swarm';
|
|
1947
|
+
const eventType = options.eventType ?? (lease.status === 'released' ? 'lease.released' : 'lease.granted');
|
|
1948
|
+
const time = swarmRunEventTime(options, eventType === 'lease.released' ? options.now : lease.leasedAt);
|
|
1949
|
+
const leaseNode = {
|
|
1950
|
+
kind: 'lease',
|
|
1951
|
+
id: swarmRunLeaseNodeId(lease.id),
|
|
1952
|
+
title: `Lease ${lease.jobId}`,
|
|
1953
|
+
scopeId: options.job ? swarmRunTaskNodeId(options.job.taskId) : swarmRunTaskNodeId(lease.jobId),
|
|
1954
|
+
leaseKey: options.job?.concurrencyKey ?? lease.jobId,
|
|
1955
|
+
holderId: lease.workerId,
|
|
1956
|
+
status: eventType === 'lease.released' ? 'released' : 'granted',
|
|
1957
|
+
requestedAt: new Date(lease.leasedAt).toISOString(),
|
|
1958
|
+
grantedAt: new Date(lease.leasedAt).toISOString(),
|
|
1959
|
+
releasedAt: eventType === 'lease.released' ? time : undefined,
|
|
1960
|
+
metadata: pruneUndefinedJsonObject({
|
|
1961
|
+
source: 'frontier-swarm.lease',
|
|
1962
|
+
jobId: lease.jobId,
|
|
1963
|
+
token: lease.token,
|
|
1964
|
+
fencingToken: lease.fencingToken,
|
|
1965
|
+
expiresAt: lease.expiresAt,
|
|
1966
|
+
status: lease.status
|
|
1967
|
+
})
|
|
1968
|
+
};
|
|
1969
|
+
return [createRunEvent({
|
|
1970
|
+
runId,
|
|
1971
|
+
actorId,
|
|
1972
|
+
actorSeq: options.startActorSeq ?? 1,
|
|
1973
|
+
parents: [...(options.parents ?? [])],
|
|
1974
|
+
time,
|
|
1975
|
+
type: eventType,
|
|
1976
|
+
payload: pruneUndefinedJsonObject({
|
|
1977
|
+
subjectId: options.job ? swarmRunTaskNodeId(options.job.taskId) : undefined,
|
|
1978
|
+
lease: leaseNode
|
|
1979
|
+
})
|
|
1980
|
+
})];
|
|
1981
|
+
}
|
|
1982
|
+
export function createRunEventsFromSwarmResult(resultInput, options = {}) {
|
|
1983
|
+
const result = isSwarmJobResult(resultInput) ? cloneJsonValue(resultInput) : normalizeResult(resultInput);
|
|
1984
|
+
const job = options.job;
|
|
1985
|
+
const runId = options.runId ?? 'frontier-swarm';
|
|
1986
|
+
const actorId = options.actorId ?? 'frontier-swarm-worker';
|
|
1987
|
+
const time = swarmRunEventTime(options, result.finishedAt ?? result.startedAt);
|
|
1988
|
+
let actorSeq = options.startActorSeq ?? 1;
|
|
1989
|
+
const parents = [...(options.parents ?? [])];
|
|
1990
|
+
const events = [];
|
|
1991
|
+
const attemptId = swarmRunAttemptNodeId(result.jobId);
|
|
1992
|
+
const taskId = swarmRunTaskNodeId(job?.taskId ?? result.jobId);
|
|
1993
|
+
const attemptEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunAttempt({
|
|
1994
|
+
id: attemptId,
|
|
1995
|
+
title: job?.title ?? result.jobId,
|
|
1996
|
+
taskId,
|
|
1997
|
+
actorId,
|
|
1998
|
+
runnerId: job?.compute.kind ?? job?.compute.id,
|
|
1999
|
+
workspaceId: job?.worktreePath,
|
|
2000
|
+
status: swarmResultStatusToAttemptStatus(result.status),
|
|
2001
|
+
startedAt: result.startedAt ? new Date(result.startedAt).toISOString() : undefined,
|
|
2002
|
+
endedAt: result.finishedAt ? new Date(result.finishedAt).toISOString() : undefined,
|
|
2003
|
+
model: job?.compute.model,
|
|
2004
|
+
reason: result.error,
|
|
2005
|
+
metadata: pruneUndefinedJsonObject({
|
|
2006
|
+
source: 'frontier-swarm.result',
|
|
2007
|
+
jobId: result.jobId,
|
|
2008
|
+
mergeReadiness: result.mergeReadiness,
|
|
2009
|
+
mergeDisposition: result.mergeDisposition,
|
|
2010
|
+
riskLevel: result.riskLevel,
|
|
2011
|
+
exitCode: result.exitCode,
|
|
2012
|
+
signal: result.signal,
|
|
2013
|
+
queueItemIds: result.queueItemIds,
|
|
2014
|
+
semanticImport: result.semanticImport,
|
|
2015
|
+
metadata: result.metadata
|
|
2016
|
+
})
|
|
2017
|
+
}), { parents, time });
|
|
2018
|
+
events.push(attemptEvent);
|
|
2019
|
+
let latestParents = [attemptEvent.id];
|
|
2020
|
+
if (result.patchPath || result.changedPaths.length) {
|
|
2021
|
+
const patchId = swarmRunPatchNodeId(result.jobId);
|
|
2022
|
+
const patchEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunPatch({
|
|
2023
|
+
id: patchId,
|
|
2024
|
+
title: `Patch ${result.jobId}`,
|
|
2025
|
+
changedPaths: result.changedPaths,
|
|
2026
|
+
artifactId: result.patchPath ? swarmRunArtifactNodeId(result.patchPath) : undefined,
|
|
2027
|
+
summary: result.lastMessage,
|
|
2028
|
+
risk: swarmRiskToRunRisk(result.riskLevel),
|
|
2029
|
+
metadata: pruneUndefinedJsonObject({
|
|
2030
|
+
source: 'frontier-swarm.result.patch',
|
|
2031
|
+
patchPath: result.patchPath,
|
|
2032
|
+
changedRegions: result.changedRegions,
|
|
2033
|
+
ownershipViolations: result.ownershipViolations
|
|
2034
|
+
})
|
|
2035
|
+
}), { parents: latestParents, time });
|
|
2036
|
+
events.push(patchEvent);
|
|
2037
|
+
events.push(createRunEdgeEvent(runId, actorId, actorSeq++, linkRunNodes(attemptId, patchId, 'produces-patch', { createdAt: time }), {
|
|
2038
|
+
parents: [patchEvent.id],
|
|
2039
|
+
time
|
|
2040
|
+
}));
|
|
2041
|
+
if (result.patchPath) {
|
|
2042
|
+
events.push(createRunEvent({
|
|
2043
|
+
runId,
|
|
2044
|
+
actorId,
|
|
2045
|
+
actorSeq: actorSeq++,
|
|
2046
|
+
parents: [patchEvent.id],
|
|
2047
|
+
time,
|
|
2048
|
+
type: 'artifact.attached',
|
|
2049
|
+
payload: {
|
|
2050
|
+
subjectId: patchId,
|
|
2051
|
+
artifact: toJsonValue(defineRunArtifact({
|
|
2052
|
+
id: swarmRunArtifactNodeId(result.patchPath),
|
|
2053
|
+
title: result.patchPath,
|
|
2054
|
+
artifactType: 'patch',
|
|
2055
|
+
path: result.patchPath,
|
|
2056
|
+
summary: `Patch artifact for ${result.jobId}`,
|
|
2057
|
+
metadata: { source: 'frontier-swarm.result.patch-artifact', jobId: result.jobId }
|
|
2058
|
+
}))
|
|
2059
|
+
}
|
|
2060
|
+
}));
|
|
2061
|
+
}
|
|
2062
|
+
latestParents = [patchEvent.id];
|
|
2063
|
+
}
|
|
2064
|
+
for (const evidencePath of result.evidencePaths) {
|
|
2065
|
+
const artifactId = swarmRunArtifactNodeId(evidencePath);
|
|
2066
|
+
const evidenceId = swarmRunEvidenceNodeId(result.jobId, evidencePath);
|
|
2067
|
+
const artifactEvent = createRunEvent({
|
|
2068
|
+
runId,
|
|
2069
|
+
actorId,
|
|
2070
|
+
actorSeq: actorSeq++,
|
|
2071
|
+
parents: latestParents,
|
|
2072
|
+
time,
|
|
2073
|
+
type: 'artifact.attached',
|
|
2074
|
+
payload: {
|
|
2075
|
+
subjectId: attemptId,
|
|
2076
|
+
artifact: toJsonValue(defineRunArtifact({
|
|
2077
|
+
id: artifactId,
|
|
2078
|
+
title: evidencePath,
|
|
2079
|
+
artifactType: inferSwarmArtifactType(evidencePath),
|
|
2080
|
+
path: evidencePath,
|
|
2081
|
+
summary: `Evidence artifact for ${result.jobId}`,
|
|
2082
|
+
metadata: { source: 'frontier-swarm.result.evidence-artifact', jobId: result.jobId }
|
|
2083
|
+
}))
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
events.push(artifactEvent);
|
|
2087
|
+
const evidenceEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunEvidence({
|
|
2088
|
+
id: evidenceId,
|
|
2089
|
+
title: evidencePath,
|
|
2090
|
+
evidenceType: inferSwarmArtifactType(evidencePath),
|
|
2091
|
+
result: result.status === 'completed' || result.status === 'verified' ? 'pass' : result.status === 'failed' ? 'fail' : 'unknown',
|
|
2092
|
+
artifactIds: [artifactId],
|
|
2093
|
+
summary: `Evidence from ${result.jobId}`,
|
|
2094
|
+
metadata: { source: 'frontier-swarm.result.evidence', jobId: result.jobId }
|
|
2095
|
+
}), { parents: [artifactEvent.id], time });
|
|
2096
|
+
events.push(evidenceEvent);
|
|
2097
|
+
events.push(createRunEdgeEvent(runId, actorId, actorSeq++, linkRunNodes(attemptId, evidenceId, 'produces-evidence', { createdAt: time }), {
|
|
2098
|
+
parents: [evidenceEvent.id],
|
|
2099
|
+
time
|
|
2100
|
+
}));
|
|
2101
|
+
}
|
|
2102
|
+
for (let index = 0; index < result.verification.length; index += 1) {
|
|
2103
|
+
const verification = result.verification[index];
|
|
2104
|
+
const verificationId = swarmRunVerificationNodeId(result.jobId, index, verification.name);
|
|
2105
|
+
const verificationEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunVerification({
|
|
2106
|
+
id: verificationId,
|
|
2107
|
+
title: verification.name,
|
|
2108
|
+
status: verification.status === 0 ? 'passed' : verification.required === false && verification.status !== 0 ? 'skipped' : 'failed',
|
|
2109
|
+
command: verification.command[0],
|
|
2110
|
+
args: verification.command.slice(1),
|
|
2111
|
+
cwd: verification.cwd,
|
|
2112
|
+
exitCode: verification.status,
|
|
2113
|
+
artifactIds: [],
|
|
2114
|
+
required: verification.required,
|
|
2115
|
+
summary: verification.commandLine,
|
|
2116
|
+
metadata: pruneUndefinedJsonObject({
|
|
2117
|
+
source: 'frontier-swarm.result.verification',
|
|
2118
|
+
jobId: result.jobId,
|
|
2119
|
+
durationMs: verification.durationMs,
|
|
2120
|
+
stdoutTail: verification.stdoutTail,
|
|
2121
|
+
stderrTail: verification.stderrTail,
|
|
2122
|
+
category: verification.category,
|
|
2123
|
+
metadata: verification.metadata
|
|
2124
|
+
})
|
|
2125
|
+
}), { parents: latestParents, time });
|
|
2126
|
+
events.push(verificationEvent);
|
|
2127
|
+
events.push(createRunEdgeEvent(runId, actorId, actorSeq++, linkRunNodes(attemptId, verificationId, 'verified-by', { createdAt: time }), {
|
|
2128
|
+
parents: [verificationEvent.id],
|
|
2129
|
+
time
|
|
2130
|
+
}));
|
|
2131
|
+
}
|
|
2132
|
+
return events;
|
|
2133
|
+
}
|
|
2134
|
+
export function createRunEventsFromMergeBundle(bundle, options = {}) {
|
|
2135
|
+
const runId = options.runId ?? bundle.runId ?? 'frontier-swarm';
|
|
2136
|
+
const actorId = options.actorId ?? 'frontier-swarm-collector';
|
|
2137
|
+
const time = swarmRunEventTime(options, bundle.generatedAt);
|
|
2138
|
+
let actorSeq = options.startActorSeq ?? 1;
|
|
2139
|
+
const parents = [...(options.parents ?? [])];
|
|
2140
|
+
const events = [];
|
|
2141
|
+
const patchId = swarmRunPatchNodeId(bundle.jobId);
|
|
2142
|
+
const patchEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunPatch({
|
|
2143
|
+
id: patchId,
|
|
2144
|
+
title: bundle.title ?? `Merge bundle ${bundle.jobId}`,
|
|
2145
|
+
changedPaths: bundle.changedPaths,
|
|
2146
|
+
artifactId: bundle.patchPath ? swarmRunArtifactNodeId(bundle.patchPath) : undefined,
|
|
2147
|
+
summary: bundle.reasons.join('; '),
|
|
2148
|
+
risk: swarmRiskToRunRisk(bundle.riskLevel),
|
|
2149
|
+
metadata: pruneUndefinedJsonObject({
|
|
2150
|
+
source: 'frontier-swarm.merge-bundle',
|
|
2151
|
+
bundleId: bundle.id,
|
|
2152
|
+
jobId: bundle.jobId,
|
|
2153
|
+
taskId: bundle.taskId,
|
|
2154
|
+
lane: bundle.lane,
|
|
2155
|
+
mergeReadiness: bundle.mergeReadiness,
|
|
2156
|
+
disposition: bundle.disposition,
|
|
2157
|
+
autoMergeable: bundle.autoMergeable,
|
|
2158
|
+
staleAgainstHead: bundle.staleAgainstHead,
|
|
2159
|
+
changedRegions: bundle.changedRegions,
|
|
2160
|
+
ownedFilesTouched: bundle.ownedFilesTouched,
|
|
2161
|
+
ownershipViolations: bundle.ownershipViolations,
|
|
2162
|
+
patchHash: bundle.patchHash,
|
|
2163
|
+
branchName: bundle.branchName,
|
|
2164
|
+
commit: bundle.commit,
|
|
2165
|
+
semanticImport: bundle.semanticImport
|
|
2166
|
+
})
|
|
2167
|
+
}), { parents, time });
|
|
2168
|
+
events.push(patchEvent);
|
|
2169
|
+
if (bundle.patchPath) {
|
|
2170
|
+
events.push(createRunEvent({
|
|
2171
|
+
runId,
|
|
2172
|
+
actorId,
|
|
2173
|
+
actorSeq: actorSeq++,
|
|
2174
|
+
parents: [patchEvent.id],
|
|
2175
|
+
time,
|
|
2176
|
+
type: 'artifact.attached',
|
|
2177
|
+
payload: {
|
|
2178
|
+
subjectId: patchId,
|
|
2179
|
+
artifact: toJsonValue(defineRunArtifact({
|
|
2180
|
+
id: swarmRunArtifactNodeId(bundle.patchPath),
|
|
2181
|
+
title: bundle.patchPath,
|
|
2182
|
+
artifactType: 'patch',
|
|
2183
|
+
path: bundle.patchPath,
|
|
2184
|
+
hash: bundle.patchHash,
|
|
2185
|
+
summary: `Merge patch for ${bundle.jobId}`,
|
|
2186
|
+
metadata: { source: 'frontier-swarm.merge-bundle.patch', bundleId: bundle.id }
|
|
2187
|
+
}))
|
|
2188
|
+
}
|
|
2189
|
+
}));
|
|
2190
|
+
}
|
|
2191
|
+
for (const evidencePath of bundle.evidencePaths) {
|
|
2192
|
+
events.push(createRunEvent({
|
|
2193
|
+
runId,
|
|
2194
|
+
actorId,
|
|
2195
|
+
actorSeq: actorSeq++,
|
|
2196
|
+
parents: [patchEvent.id],
|
|
2197
|
+
time,
|
|
2198
|
+
type: 'artifact.attached',
|
|
2199
|
+
payload: {
|
|
2200
|
+
subjectId: patchId,
|
|
2201
|
+
artifact: toJsonValue(defineRunArtifact({
|
|
2202
|
+
id: swarmRunArtifactNodeId(evidencePath),
|
|
2203
|
+
title: evidencePath,
|
|
2204
|
+
artifactType: inferSwarmArtifactType(evidencePath),
|
|
2205
|
+
path: evidencePath,
|
|
2206
|
+
summary: `Merge evidence for ${bundle.jobId}`,
|
|
2207
|
+
metadata: { source: 'frontier-swarm.merge-bundle.evidence', bundleId: bundle.id }
|
|
2208
|
+
}))
|
|
2209
|
+
}
|
|
2210
|
+
}));
|
|
2211
|
+
}
|
|
2212
|
+
const decisionKind = swarmDispositionToRunDecision(bundle.disposition, bundle.mergeReadiness);
|
|
2213
|
+
const decisionEvent = createRunEvent({
|
|
2214
|
+
runId,
|
|
2215
|
+
actorId,
|
|
2216
|
+
actorSeq: actorSeq++,
|
|
2217
|
+
parents: [patchEvent.id],
|
|
2218
|
+
time,
|
|
2219
|
+
type: 'decision.recorded',
|
|
2220
|
+
payload: {
|
|
2221
|
+
decision: toJsonValue(defineRunDecision({
|
|
2222
|
+
id: swarmRunDecisionNodeId(bundle.id || bundle.jobId),
|
|
2223
|
+
title: `Merge decision ${bundle.jobId}`,
|
|
2224
|
+
decision: decisionKind,
|
|
2225
|
+
subjectIds: uniqueStrings([patchId, bundle.taskId ? swarmRunTaskNodeId(bundle.taskId) : undefined]),
|
|
2226
|
+
actorId,
|
|
2227
|
+
reason: bundle.reasons.join('; ') || bundle.disposition,
|
|
2228
|
+
requiredActions: bundle.disposition === 'needs-port' ? ['human-port'] : bundle.staleAgainstHead ? ['rerun-against-head'] : [],
|
|
2229
|
+
metadata: pruneUndefinedJsonObject({
|
|
2230
|
+
source: 'frontier-swarm.merge-bundle.decision',
|
|
2231
|
+
bundleId: bundle.id,
|
|
2232
|
+
disposition: bundle.disposition,
|
|
2233
|
+
mergeReadiness: bundle.mergeReadiness,
|
|
2234
|
+
riskLevel: bundle.riskLevel,
|
|
2235
|
+
autoMergeable: bundle.autoMergeable
|
|
2236
|
+
})
|
|
2237
|
+
}))
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
events.push(decisionEvent);
|
|
2241
|
+
return events;
|
|
2242
|
+
}
|
|
2243
|
+
export function createRunEventsFromCoordinatorDecision(decision, options = {}) {
|
|
2244
|
+
const normalized = createSwarmQueueOutcomeDecision(decision);
|
|
2245
|
+
const runId = options.runId ?? 'frontier-swarm';
|
|
2246
|
+
const actorId = options.actorId ?? 'frontier-swarm-coordinator';
|
|
2247
|
+
const time = swarmRunEventTime(options, normalized.generatedAt);
|
|
2248
|
+
const subjectIds = uniqueStrings([
|
|
2249
|
+
normalized.jobId ? swarmRunAttemptNodeId(normalized.jobId) : undefined,
|
|
2250
|
+
normalized.taskId ? swarmRunTaskNodeId(normalized.taskId) : undefined,
|
|
2251
|
+
normalized.subjectId
|
|
2252
|
+
]);
|
|
2253
|
+
return [createRunEvent({
|
|
2254
|
+
runId,
|
|
2255
|
+
actorId,
|
|
2256
|
+
actorSeq: options.startActorSeq ?? 1,
|
|
2257
|
+
parents: [...(options.parents ?? [])],
|
|
2258
|
+
time,
|
|
2259
|
+
type: 'decision.recorded',
|
|
2260
|
+
payload: {
|
|
2261
|
+
decision: toJsonValue(defineRunDecision({
|
|
2262
|
+
id: swarmRunDecisionNodeId(normalized.id),
|
|
2263
|
+
title: `Coordinator decision ${normalized.subjectId}`,
|
|
2264
|
+
decision: swarmQueueOutcomeToRunDecision(normalized),
|
|
2265
|
+
subjectIds,
|
|
2266
|
+
actorId,
|
|
2267
|
+
reason: normalized.reasons.join('; ') || normalized.outcome,
|
|
2268
|
+
requiredActions: swarmQueueOutcomeRequiredActions(normalized),
|
|
2269
|
+
metadata: pruneUndefinedJsonObject({
|
|
2270
|
+
source: 'frontier-swarm.queue-outcome-decision',
|
|
2271
|
+
decision: normalized.decision,
|
|
2272
|
+
action: normalized.action,
|
|
2273
|
+
assignedAction: normalized.assignedAction,
|
|
2274
|
+
category: normalized.category,
|
|
2275
|
+
outcome: normalized.outcome,
|
|
2276
|
+
terminal: normalized.terminal,
|
|
2277
|
+
queueItemIds: normalized.queueItemIds,
|
|
2278
|
+
queueId: normalized.queueId,
|
|
2279
|
+
lane: normalized.lane,
|
|
2280
|
+
disposition: normalized.disposition,
|
|
2281
|
+
mergeReadiness: normalized.mergeReadiness,
|
|
2282
|
+
status: normalized.status,
|
|
2283
|
+
conflictingJobIds: normalized.conflictingJobIds,
|
|
2284
|
+
metadata: normalized.metadata
|
|
2285
|
+
})
|
|
2286
|
+
}))
|
|
2287
|
+
}
|
|
2288
|
+
})];
|
|
2289
|
+
}
|
|
2290
|
+
export function createRunProjectionFromSwarmRunEvents(events, options = {}) {
|
|
2291
|
+
return replayRunEvents(events, {
|
|
2292
|
+
id: options.runId,
|
|
2293
|
+
goal: options.goal,
|
|
2294
|
+
metadata: toJsonObject(options.metadata)
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
export function createRunDashboardFromSwarmRun(input, options = {}) {
|
|
2298
|
+
const events = isFrontierRunEventList(input) ? input : createRunEventsFromSwarmRun(input, options);
|
|
2299
|
+
return createRunDashboardSnapshot(createRunProjectionFromSwarmRunEvents(events, options));
|
|
2300
|
+
}
|
|
2301
|
+
export function createRunEventsFromSwarmRun(run, options = {}) {
|
|
2302
|
+
const runId = options.runId ?? run.id;
|
|
2303
|
+
const actorId = options.actorId ?? 'frontier-swarm';
|
|
2304
|
+
const created = createRunEvent({
|
|
2305
|
+
runId,
|
|
2306
|
+
actorId,
|
|
2307
|
+
actorSeq: options.startActorSeq ?? 1,
|
|
2308
|
+
parents: [...(options.parents ?? [])],
|
|
2309
|
+
time: swarmRunEventTime(options, run.startedAt),
|
|
2310
|
+
type: 'run.created',
|
|
2311
|
+
payload: {
|
|
2312
|
+
goal: run.metadata?.objective ?? run.metadata?.goal ?? `Frontier swarm ${run.id}`,
|
|
2313
|
+
metadata: pruneUndefinedJsonObject({
|
|
2314
|
+
source: 'frontier-swarm.run',
|
|
2315
|
+
planId: run.planId,
|
|
2316
|
+
manifestId: run.manifestId,
|
|
2317
|
+
status: run.status,
|
|
2318
|
+
summary: run.summary,
|
|
2319
|
+
metadata: run.metadata
|
|
2320
|
+
})
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
return [
|
|
2324
|
+
created,
|
|
2325
|
+
...run.results.flatMap((result, index) => createRunEventsFromSwarmResult(result, {
|
|
2326
|
+
runId,
|
|
2327
|
+
actorId: 'frontier-swarm-worker',
|
|
2328
|
+
startActorSeq: 1000 + index * 100,
|
|
2329
|
+
parents: [created.id],
|
|
2330
|
+
job: run.jobs.find((job) => job.id === result.jobId)
|
|
2331
|
+
}))
|
|
2332
|
+
];
|
|
2333
|
+
}
|
|
753
2334
|
export function recordSwarmEvent(runInput, eventInput) {
|
|
754
2335
|
const run = cloneJsonValue(runInput);
|
|
755
2336
|
run.events = run.events.concat(normalizeEvent({ ...eventInput, runId: eventInput.runId ?? run.id }));
|
|
@@ -1030,6 +2611,14 @@ const SEMANTIC_IMPORT_CONFLICT_HINT_KEYS = new Set([
|
|
|
1030
2611
|
'symbols',
|
|
1031
2612
|
'touchedSymbols'
|
|
1032
2613
|
]);
|
|
2614
|
+
const SEMANTIC_IMPORT_PUBLIC_CONTRACT_HINT_KEYS = new Set([
|
|
2615
|
+
'apiContract',
|
|
2616
|
+
'apiContracts',
|
|
2617
|
+
'publicContract',
|
|
2618
|
+
'publicContractId',
|
|
2619
|
+
'publicContractIds',
|
|
2620
|
+
'publicContracts'
|
|
2621
|
+
]);
|
|
1033
2622
|
function semanticImportExportMetadata(value, key, file) {
|
|
1034
2623
|
const explicitKind = semanticImportStringValue(value.kind);
|
|
1035
2624
|
const declarationKind = semanticImportStringValue(value.declarationKind)
|
|
@@ -1119,9 +2708,28 @@ function semanticImportStringValue(value) {
|
|
|
1119
2708
|
function semanticMergeConflictKeys(semanticImport) {
|
|
1120
2709
|
const records = semanticImportRecords(semanticImport);
|
|
1121
2710
|
const sources = records.length > 0 ? records : [semanticImport];
|
|
1122
|
-
return uniqueStrings(sources.flatMap((source) =>
|
|
2711
|
+
return uniqueStrings(sources.flatMap((source) => semanticImportConflictKeyStringsForValue('mergeCandidate' in source && source.mergeCandidate !== undefined
|
|
1123
2712
|
? source.mergeCandidate
|
|
1124
|
-
: source)
|
|
2713
|
+
: source)));
|
|
2714
|
+
}
|
|
2715
|
+
function semanticImportConflictKeyStringsForValue(value, key, depth = 0) {
|
|
2716
|
+
if (value === undefined || value === null)
|
|
2717
|
+
return [];
|
|
2718
|
+
if (typeof value === 'string') {
|
|
2719
|
+
const normalized = value.trim();
|
|
2720
|
+
if (!normalized)
|
|
2721
|
+
return [];
|
|
2722
|
+
if (key && SEMANTIC_IMPORT_PUBLIC_CONTRACT_HINT_KEYS.has(key))
|
|
2723
|
+
return [`public-contract:${publicContractConflictValue(normalized)}`];
|
|
2724
|
+
if (key && SEMANTIC_IMPORT_CONFLICT_HINT_KEYS.has(key))
|
|
2725
|
+
return [`symbol:${normalized}`];
|
|
2726
|
+
return [];
|
|
2727
|
+
}
|
|
2728
|
+
if (depth >= 4 || typeof value !== 'object')
|
|
2729
|
+
return [];
|
|
2730
|
+
if (Array.isArray(value))
|
|
2731
|
+
return value.flatMap((item) => semanticImportConflictKeyStringsForValue(item, key, depth + 1));
|
|
2732
|
+
return Object.entries(value).flatMap(([nextKey, nextValue]) => (semanticImportConflictKeyStringsForValue(nextValue, nextKey, depth + 1)));
|
|
1125
2733
|
}
|
|
1126
2734
|
function semanticImportConflictHintStringsForValue(value, key, depth = 0) {
|
|
1127
2735
|
if (value === undefined || value === null)
|
|
@@ -2879,7 +4487,8 @@ export function classifySwarmQueueOutcome(input) {
|
|
|
2879
4487
|
|| action === 'reject'
|
|
2880
4488
|
|| action === 'record-only'
|
|
2881
4489
|
|| input.disposition === 'rejected'
|
|
2882
|
-
|| input.mergeReadiness === 'discovery-only'
|
|
4490
|
+
|| input.mergeReadiness === 'discovery-only'
|
|
4491
|
+
|| queueOutcomeHas(search, 'research-complete', 'discovery-complete', 'synthesized')) {
|
|
2883
4492
|
category = 'terminal';
|
|
2884
4493
|
}
|
|
2885
4494
|
else if (action === 'rerun'
|
|
@@ -3137,6 +4746,8 @@ function terminalOutcomeLabelFromText(value) {
|
|
|
3137
4746
|
return 'evidence-only';
|
|
3138
4747
|
if (token === 'no-change' || token === 'nochange' || token === 'no-op' || token === 'noop' || token === 'unchanged')
|
|
3139
4748
|
return 'no-change';
|
|
4749
|
+
if (token === 'research-complete' || token === 'discovery-complete' || token === 'synthesized')
|
|
4750
|
+
return 'research-complete';
|
|
3140
4751
|
if (token === 'generated-by-collector' || token === 'collector-generated' || token === 'generated-collector')
|
|
3141
4752
|
return 'generated-by-collector';
|
|
3142
4753
|
if (token === 'patch-missing' || token === 'missing-patch' || token === 'patchmissing')
|
|
@@ -3149,7 +4760,7 @@ function terminalOutcomeLabelFromText(value) {
|
|
|
3149
4760
|
return 'rejected';
|
|
3150
4761
|
if (token === 'conflict-blocked' || token === 'merge-conflict' || token === 'textual-conflict' || token === 'conflict')
|
|
3151
4762
|
return 'conflict-blocked';
|
|
3152
|
-
if (token === 'human-question' || token === 'human-blocked' || token === 'blocked')
|
|
4763
|
+
if (token === 'human-needed' || token === 'human-question' || token === 'human-blocked' || token === 'blocked')
|
|
3153
4764
|
return 'human-question';
|
|
3154
4765
|
if (token === 'coordinator-review' || token === 'needs-port' || token === 'escalated' || token === 'review')
|
|
3155
4766
|
return 'coordinator-review';
|
|
@@ -3178,6 +4789,12 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
|
|
|
3178
4789
|
|| terminalOutcomeTextMatches(input.outcome, 'no-change', 'nochange', 'no-op', 'noop', 'unchanged')
|
|
3179
4790
|
|| terminalOutcomeTextMatches(input.status, 'no-change', 'nochange', 'no-op', 'noop', 'unchanged')
|
|
3180
4791
|
|| terminalOutcomeTextMatches(input.decision, 'no-change', 'nochange', 'no-op', 'noop', 'unchanged');
|
|
4792
|
+
const researchComplete = typeof input === 'string'
|
|
4793
|
+
? terminalOutcomeTextMatches(input, 'research-complete', 'discovery-complete', 'synthesized')
|
|
4794
|
+
: terminalOutcomeTextMatches(input.label, 'research-complete', 'discovery-complete', 'synthesized')
|
|
4795
|
+
|| terminalOutcomeTextMatches(input.outcome, 'research-complete', 'discovery-complete', 'synthesized')
|
|
4796
|
+
|| terminalOutcomeTextMatches(input.status, 'research-complete', 'discovery-complete', 'synthesized')
|
|
4797
|
+
|| terminalOutcomeTextMatches(input.decision, 'research-complete', 'discovery-complete', 'synthesized');
|
|
3181
4798
|
const patchMissing = typeof input === 'string'
|
|
3182
4799
|
? terminalOutcomeTextMatches(input, 'patch-missing', 'missing-patch', 'patchmissing')
|
|
3183
4800
|
: input.patchMissing === true
|
|
@@ -3200,8 +4817,12 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
|
|
|
3200
4817
|
|| terminalOutcomeTextMatches(input.status, 'conflict-blocked', 'merge-conflict', 'textual-conflict', 'conflict')
|
|
3201
4818
|
|| terminalOutcomeTextMatches(input.decision, 'conflict-blocked', 'merge-conflict', 'textual-conflict', 'conflict');
|
|
3202
4819
|
const humanQuestion = typeof input === 'string'
|
|
3203
|
-
? terminalOutcomeTextMatches(input, 'human-question')
|
|
4820
|
+
? terminalOutcomeTextMatches(input, 'human-needed', 'human-question')
|
|
3204
4821
|
: input.humanQuestion === true
|
|
4822
|
+
|| terminalOutcomeTextMatches(input.label, 'human-needed')
|
|
4823
|
+
|| terminalOutcomeTextMatches(input.outcome, 'human-needed')
|
|
4824
|
+
|| terminalOutcomeTextMatches(input.status, 'human-needed')
|
|
4825
|
+
|| terminalOutcomeTextMatches(input.decision, 'human-needed')
|
|
3205
4826
|
|| terminalOutcomeTextMatches(input.label, 'human-question')
|
|
3206
4827
|
|| terminalOutcomeTextMatches(input.outcome, 'human-question')
|
|
3207
4828
|
|| terminalOutcomeTextMatches(input.status, 'human-question')
|
|
@@ -3227,18 +4848,20 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
|
|
|
3227
4848
|
? 'patch-missing'
|
|
3228
4849
|
: evidenceOnly
|
|
3229
4850
|
? 'evidence-only'
|
|
3230
|
-
:
|
|
3231
|
-
? '
|
|
3232
|
-
:
|
|
3233
|
-
? '
|
|
3234
|
-
:
|
|
3235
|
-
? '
|
|
3236
|
-
:
|
|
3237
|
-
? '
|
|
3238
|
-
:
|
|
3239
|
-
? '
|
|
3240
|
-
:
|
|
3241
|
-
|
|
4851
|
+
: researchComplete
|
|
4852
|
+
? 'research-complete'
|
|
4853
|
+
: noChange
|
|
4854
|
+
? 'no-change'
|
|
4855
|
+
: generatedByCollector
|
|
4856
|
+
? 'generated-by-collector'
|
|
4857
|
+
: conflictBlocked
|
|
4858
|
+
? 'conflict-blocked'
|
|
4859
|
+
: humanQuestion || humanBlocked
|
|
4860
|
+
? 'human-question'
|
|
4861
|
+
: coordinatorReview
|
|
4862
|
+
? 'coordinator-review'
|
|
4863
|
+
: explicitLabel ?? 'unknown';
|
|
4864
|
+
const category = label === 'applied' || label === 'committed' || label === 'checked' || label === 'superseded' || label === 'evidence-only' || label === 'no-change' || label === 'research-complete' || label === 'generated-by-collector'
|
|
3242
4865
|
? 'success'
|
|
3243
4866
|
: label === 'patch-missing' || label === 'bundle-missing'
|
|
3244
4867
|
? 'incomplete'
|
|
@@ -3264,6 +4887,65 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
|
|
|
3264
4887
|
...(typeof input !== 'string' && toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
3265
4888
|
};
|
|
3266
4889
|
}
|
|
4890
|
+
export function createSwarmTerminalOutcomeRecord(input = {}) {
|
|
4891
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
4892
|
+
const queueItemIds = uniqueStrings([input.queueItemId, ...(input.queueItemIds ?? [])]);
|
|
4893
|
+
const subjectId = input.subjectId?.trim()
|
|
4894
|
+
|| queueItemIds[0]
|
|
4895
|
+
|| input.taskId?.trim()
|
|
4896
|
+
|| input.jobId?.trim()
|
|
4897
|
+
|| input.id?.trim()
|
|
4898
|
+
|| 'unknown-subject';
|
|
4899
|
+
const subjectAliases = uniqueStrings([
|
|
4900
|
+
input.subjectId,
|
|
4901
|
+
...(input.subjectAliases ?? []),
|
|
4902
|
+
...queueItemIds,
|
|
4903
|
+
input.taskId,
|
|
4904
|
+
input.jobId
|
|
4905
|
+
]);
|
|
4906
|
+
const outcome = normalizeSwarmTerminalOutcome(terminalOutcomeInputForRecord(input));
|
|
4907
|
+
const status = terminalOutcomeRecordStatusFromText(input.status)
|
|
4908
|
+
?? terminalOutcomeRecordStatusFromOutcome(outcome);
|
|
4909
|
+
const reasonCodes = normalizeSwarmTerminalOutcomeReasonCodes([
|
|
4910
|
+
...(input.reasonCodes ?? []),
|
|
4911
|
+
...defaultTerminalOutcomeRecordReasonCodes(status)
|
|
4912
|
+
]);
|
|
4913
|
+
const admitted = input.admitted ?? terminalOutcomeRecordStatusIsAdmitted(status);
|
|
4914
|
+
const evidenceRefs = normalizeNamedRefs(input.evidenceRefs ?? [], 'evidence');
|
|
4915
|
+
return {
|
|
4916
|
+
kind: FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_KIND,
|
|
4917
|
+
version: FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_VERSION,
|
|
4918
|
+
id: input.id ?? 'swarm-terminal-outcome-record:' + stableHash([subjectId, subjectAliases, status, reasonCodes, generatedAt]),
|
|
4919
|
+
generatedAt,
|
|
4920
|
+
subjectId,
|
|
4921
|
+
subjectAliases: subjectAliases.length ? subjectAliases : [subjectId],
|
|
4922
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
4923
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
4924
|
+
...(input.queueId ? { queueId: input.queueId } : {}),
|
|
4925
|
+
queueItemIds,
|
|
4926
|
+
...(input.lane ? { lane: input.lane } : {}),
|
|
4927
|
+
...(input.source ? { source: input.source } : {}),
|
|
4928
|
+
status,
|
|
4929
|
+
outcome,
|
|
4930
|
+
terminal: true,
|
|
4931
|
+
admitted,
|
|
4932
|
+
success: outcome.success,
|
|
4933
|
+
rejected: status === 'rejected',
|
|
4934
|
+
conflict: status === 'conflict',
|
|
4935
|
+
humanNeeded: status === 'human-needed',
|
|
4936
|
+
rerun: status === 'rerun',
|
|
4937
|
+
noChange: status === 'no-change',
|
|
4938
|
+
researchComplete: status === 'research-complete',
|
|
4939
|
+
reasonCodes,
|
|
4940
|
+
reasons: uniqueStrings(input.reasons ?? []),
|
|
4941
|
+
conflictingJobIds: uniqueStrings(input.conflictingJobIds ?? []),
|
|
4942
|
+
supersedes: uniqueStrings(input.supersedes ?? []),
|
|
4943
|
+
...(input.supersededBy ? { supersededBy: input.supersededBy } : {}),
|
|
4944
|
+
...(input.rerunOf ? { rerunOf: input.rerunOf } : {}),
|
|
4945
|
+
evidenceRefs,
|
|
4946
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
4947
|
+
};
|
|
4948
|
+
}
|
|
3267
4949
|
export function reconcileSwarmTerminalState(input = {}) {
|
|
3268
4950
|
const generatedAt = input.generatedAt ?? Date.now();
|
|
3269
4951
|
const doneBucket = input.doneBucket ?? 'done';
|
|
@@ -4054,13 +5736,25 @@ function createMergeIndexConflicts(entries) {
|
|
|
4054
5736
|
continue;
|
|
4055
5737
|
const keys = pairConflictKeys(left, right);
|
|
4056
5738
|
for (const key of keys) {
|
|
4057
|
-
const kind = key.startsWith('symbol:')
|
|
5739
|
+
const kind = key.startsWith('symbol:')
|
|
5740
|
+
? 'symbol'
|
|
5741
|
+
: key.startsWith('public-contract:')
|
|
5742
|
+
? 'public-contract'
|
|
5743
|
+
: key.startsWith('region:')
|
|
5744
|
+
? 'region'
|
|
5745
|
+
: 'path';
|
|
4058
5746
|
const value = key.slice(key.indexOf(':') + 1);
|
|
4059
5747
|
conflicts.push({
|
|
4060
5748
|
jobIds: [left.jobId, right.jobId].sort(),
|
|
4061
5749
|
key,
|
|
4062
5750
|
kind,
|
|
4063
|
-
...(kind === 'region'
|
|
5751
|
+
...(kind === 'region'
|
|
5752
|
+
? { region: value }
|
|
5753
|
+
: kind === 'symbol'
|
|
5754
|
+
? { symbol: value }
|
|
5755
|
+
: kind === 'public-contract'
|
|
5756
|
+
? { publicContract: value }
|
|
5757
|
+
: { path: value })
|
|
4064
5758
|
});
|
|
4065
5759
|
}
|
|
4066
5760
|
}
|
|
@@ -4077,6 +5771,9 @@ function pairConflictKeys(left, right) {
|
|
|
4077
5771
|
const sharedSymbols = leftKeys.filter((key) => key.startsWith('symbol:') && rightKeySet.has(key));
|
|
4078
5772
|
if (sharedSymbols.length > 0)
|
|
4079
5773
|
return sharedSymbols.sort();
|
|
5774
|
+
const sharedPublicContracts = leftKeys.filter((key) => key.startsWith('public-contract:') && rightKeySet.has(key));
|
|
5775
|
+
if (sharedPublicContracts.length > 0)
|
|
5776
|
+
return sharedPublicContracts.sort();
|
|
4080
5777
|
const leftRegions = leftKeys.filter((key) => key.startsWith('region:'));
|
|
4081
5778
|
const rightRegions = rightKeys.filter((key) => key.startsWith('region:'));
|
|
4082
5779
|
if (leftRegions.length > 0 && rightRegions.length > 0) {
|
|
@@ -4137,6 +5834,11 @@ function stableIdPart(value) {
|
|
|
4137
5834
|
return 'star';
|
|
4138
5835
|
return normalized.length > 0 ? normalized : 'anonymous';
|
|
4139
5836
|
}
|
|
5837
|
+
function publicContractConflictValue(value) {
|
|
5838
|
+
const trimmed = value.trim();
|
|
5839
|
+
const withoutPrefix = trimmed.startsWith('public-contract:') ? trimmed.slice('public-contract:'.length) : trimmed;
|
|
5840
|
+
return stableIdPart(withoutPrefix);
|
|
5841
|
+
}
|
|
4140
5842
|
function reviewerLaneReasons(entry) {
|
|
4141
5843
|
const reasons = [];
|
|
4142
5844
|
if (entry.conflictingJobIds.length)
|
|
@@ -5186,6 +6888,8 @@ function defaultQueueOutcomeForCategory(category, input, search) {
|
|
|
5186
6888
|
return 'committed';
|
|
5187
6889
|
if (action === 'apply-local' || input.decision === 'applied' || queueOutcomeHas(search, 'applied', 'apply-local', 'apply'))
|
|
5188
6890
|
return 'applied';
|
|
6891
|
+
if (input.decision === 'research-complete' || queueOutcomeHas(search, 'research-complete', 'discovery-complete', 'synthesized'))
|
|
6892
|
+
return 'research-complete';
|
|
5189
6893
|
if (input.decision === 'checked' || queueOutcomeHas(search, 'checked', 'check'))
|
|
5190
6894
|
return 'checked';
|
|
5191
6895
|
if (input.decision === 'superseded' || queueOutcomeHas(search, 'superseded'))
|
|
@@ -5249,6 +6953,8 @@ function canonicalizeSwarmQueueOutcome(value) {
|
|
|
5249
6953
|
return 'superseded';
|
|
5250
6954
|
if (token === 'no-change' || token === 'nochange' || token === 'no-op' || token === 'noop' || token === 'unchanged')
|
|
5251
6955
|
return 'no-change';
|
|
6956
|
+
if (token === 'research-complete' || token === 'discovery-complete' || token === 'synthesized')
|
|
6957
|
+
return 'research-complete';
|
|
5252
6958
|
if (token === 'rejected' || token === 'reject' || token === 'failed' || token === 'failure')
|
|
5253
6959
|
return 'rejected';
|
|
5254
6960
|
if (token === 'rerun' || token === 're-run' || token === 'retry' || token === 'needs-rerun' || token === 'stale-rerun')
|
|
@@ -5263,6 +6969,96 @@ function canonicalizeSwarmQueueOutcome(value) {
|
|
|
5263
6969
|
return 'closed';
|
|
5264
6970
|
return undefined;
|
|
5265
6971
|
}
|
|
6972
|
+
function terminalOutcomeInputForRecord(input) {
|
|
6973
|
+
if (input.outcome && typeof input.outcome === 'object')
|
|
6974
|
+
return input.outcome;
|
|
6975
|
+
return {
|
|
6976
|
+
label: input.label,
|
|
6977
|
+
outcome: typeof input.outcome === 'string' ? input.outcome : undefined,
|
|
6978
|
+
status: input.status,
|
|
6979
|
+
decision: input.decision,
|
|
6980
|
+
reasons: input.reasons,
|
|
6981
|
+
metadata: input.metadata
|
|
6982
|
+
};
|
|
6983
|
+
}
|
|
6984
|
+
function terminalOutcomeRecordStatusFromText(value) {
|
|
6985
|
+
const normalized = normalizeOptionalString(value);
|
|
6986
|
+
if (!normalized)
|
|
6987
|
+
return undefined;
|
|
6988
|
+
const token = normalizeSwarmTerminalOutcomeText(normalized);
|
|
6989
|
+
if (token === 'applied' || token === 'apply-local' || token === 'apply' || token === 'committed' || token === 'commit' || token === 'checked' || token === 'check')
|
|
6990
|
+
return 'applied';
|
|
6991
|
+
if (token === 'rejected' || token === 'reject' || token === 'failed' || token === 'failure')
|
|
6992
|
+
return 'rejected';
|
|
6993
|
+
if (token === 'superseded')
|
|
6994
|
+
return 'superseded';
|
|
6995
|
+
if (token === 'no-change' || token === 'nochange' || token === 'no-op' || token === 'noop' || token === 'unchanged' || token === 'recorded' || token === 'closed' || token === 'evidence-only' || token === 'evidenceonly' || token === 'generated-by-collector')
|
|
6996
|
+
return 'no-change';
|
|
6997
|
+
if (token === 'conflict' || token === 'conflict-blocked' || token === 'merge-conflict' || token === 'textual-conflict')
|
|
6998
|
+
return 'conflict';
|
|
6999
|
+
if (token === 'human-needed' || token === 'human-question' || token === 'human-blocked' || token === 'blocked' || token === 'coordinator-review' || token === 'needs-port' || token === 'escalated' || token === 'review')
|
|
7000
|
+
return 'human-needed';
|
|
7001
|
+
if (token === 'research-complete' || token === 'discovery-complete' || token === 'synthesized')
|
|
7002
|
+
return 'research-complete';
|
|
7003
|
+
if (token === 'rerun' || token === 're-run' || token === 'retry' || token === 'needs-rerun' || token === 'stale-rerun')
|
|
7004
|
+
return 'rerun';
|
|
7005
|
+
return token;
|
|
7006
|
+
}
|
|
7007
|
+
function terminalOutcomeRecordStatusFromOutcome(outcome) {
|
|
7008
|
+
return terminalOutcomeRecordStatusFromText(outcome.label)
|
|
7009
|
+
?? terminalOutcomeRecordStatusFromText(outcome.category)
|
|
7010
|
+
?? 'no-change';
|
|
7011
|
+
}
|
|
7012
|
+
function terminalOutcomeRecordStatusIsAdmitted(status) {
|
|
7013
|
+
return status === 'applied'
|
|
7014
|
+
|| status === 'superseded'
|
|
7015
|
+
|| status === 'no-change'
|
|
7016
|
+
|| status === 'research-complete';
|
|
7017
|
+
}
|
|
7018
|
+
function defaultTerminalOutcomeRecordReasonCodes(status) {
|
|
7019
|
+
if (status === 'applied')
|
|
7020
|
+
return ['accepted-by-admission'];
|
|
7021
|
+
if (status === 'rejected')
|
|
7022
|
+
return ['failed-verification'];
|
|
7023
|
+
if (status === 'superseded')
|
|
7024
|
+
return ['superseded-by-newer-output'];
|
|
7025
|
+
if (status === 'no-change')
|
|
7026
|
+
return ['no-effective-change'];
|
|
7027
|
+
if (status === 'conflict')
|
|
7028
|
+
return ['conflict-detected'];
|
|
7029
|
+
if (status === 'human-needed')
|
|
7030
|
+
return ['human-decision-required'];
|
|
7031
|
+
if (status === 'research-complete')
|
|
7032
|
+
return ['research-complete'];
|
|
7033
|
+
if (status === 'rerun')
|
|
7034
|
+
return ['stale-rerun-required'];
|
|
7035
|
+
return [];
|
|
7036
|
+
}
|
|
7037
|
+
function normalizeSwarmTerminalOutcomeReasonCodes(reasonCodes) {
|
|
7038
|
+
return uniqueStrings(uniqueStrings(reasonCodes).map(canonicalizeSwarmTerminalOutcomeReasonCode));
|
|
7039
|
+
}
|
|
7040
|
+
function canonicalizeSwarmTerminalOutcomeReasonCode(value) {
|
|
7041
|
+
const token = normalizeSwarmTerminalOutcomeText(value);
|
|
7042
|
+
if (!token)
|
|
7043
|
+
return value;
|
|
7044
|
+
if (token === 'accepted' || token === 'admitted' || token === 'accepted-by-admission')
|
|
7045
|
+
return 'accepted-by-admission';
|
|
7046
|
+
if (token === 'failed' || token === 'failed-verification' || token === 'verification-failed')
|
|
7047
|
+
return 'failed-verification';
|
|
7048
|
+
if (token === 'superseded' || token === 'superseded-by-newer-output')
|
|
7049
|
+
return 'superseded-by-newer-output';
|
|
7050
|
+
if (token === 'no-change' || token === 'nochange' || token === 'no-effective-change')
|
|
7051
|
+
return 'no-effective-change';
|
|
7052
|
+
if (token === 'conflict' || token === 'conflict-detected' || token === 'merge-conflict')
|
|
7053
|
+
return 'conflict-detected';
|
|
7054
|
+
if (token === 'human-needed' || token === 'human-decision-required' || token === 'human-question')
|
|
7055
|
+
return 'human-decision-required';
|
|
7056
|
+
if (token === 'research-complete' || token === 'discovery-complete')
|
|
7057
|
+
return 'research-complete';
|
|
7058
|
+
if (token === 'rerun' || token === 'stale-rerun-required' || token === 'needs-rerun')
|
|
7059
|
+
return 'stale-rerun-required';
|
|
7060
|
+
return token;
|
|
7061
|
+
}
|
|
5266
7062
|
function queueOutcomeInputsFromMergeQueue(queue) {
|
|
5267
7063
|
return queue.assignments.map((assignment) => ({
|
|
5268
7064
|
subjectAliases: uniqueStrings([assignment.taskId, assignment.jobId, ...assignment.queueItemIds]),
|
|
@@ -5420,6 +7216,7 @@ function queueOutcomeDecisionIsResolvedOutput(decision) {
|
|
|
5420
7216
|
|| decision.outcome === 'checked'
|
|
5421
7217
|
|| decision.outcome === 'superseded'
|
|
5422
7218
|
|| decision.outcome === 'no-change'
|
|
7219
|
+
|| decision.outcome === 'research-complete'
|
|
5423
7220
|
|| decision.outcome === 'recorded'
|
|
5424
7221
|
|| decision.outcome === 'closed';
|
|
5425
7222
|
}
|
|
@@ -5867,6 +7664,253 @@ function countBy(values) {
|
|
|
5867
7664
|
out[value] = (out[value] ?? 0) + 1;
|
|
5868
7665
|
return out;
|
|
5869
7666
|
}
|
|
7667
|
+
const FRONTIER_SWARM_BLOCKING_MERGE_CANDIDATE_REASON_CODES = new Set([
|
|
7668
|
+
'missing-sidecar',
|
|
7669
|
+
'empty-sidecar',
|
|
7670
|
+
'stale-source-hash',
|
|
7671
|
+
'symbol-conflict',
|
|
7672
|
+
'effect-conflict'
|
|
7673
|
+
]);
|
|
7674
|
+
const FRONTIER_SWARM_REVIEW_MERGE_CANDIDATE_REASON_CODES = new Set([
|
|
7675
|
+
'tests-missing'
|
|
7676
|
+
]);
|
|
7677
|
+
const FRONTIER_SWARM_CONFLICT_MERGE_CANDIDATE_REASON_CODES = new Set([
|
|
7678
|
+
'symbol-conflict',
|
|
7679
|
+
'effect-conflict'
|
|
7680
|
+
]);
|
|
7681
|
+
function isSwarmSemanticChange(input) {
|
|
7682
|
+
return input.kind === FRONTIER_SWARM_SEMANTIC_CHANGE_KIND;
|
|
7683
|
+
}
|
|
7684
|
+
function isSwarmMergeCandidate(input) {
|
|
7685
|
+
return input.kind === FRONTIER_SWARM_MERGE_CANDIDATE_KIND;
|
|
7686
|
+
}
|
|
7687
|
+
function normalizeOptionalString(value) {
|
|
7688
|
+
if (typeof value !== 'string')
|
|
7689
|
+
return undefined;
|
|
7690
|
+
const normalized = value.trim();
|
|
7691
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
7692
|
+
}
|
|
7693
|
+
function normalizeSwarmMergeCandidateReasonCodes(reasonCodes) {
|
|
7694
|
+
return uniqueStrings(reasonCodes);
|
|
7695
|
+
}
|
|
7696
|
+
function normalizeSwarmSourceSpan(input, fallback = {}) {
|
|
7697
|
+
const metadata = toJsonObject(input?.metadata);
|
|
7698
|
+
const startLine = readNonNegativeNumber(input?.startLine);
|
|
7699
|
+
const startColumn = readNonNegativeNumber(input?.startColumn);
|
|
7700
|
+
const endLine = readNonNegativeNumber(input?.endLine);
|
|
7701
|
+
const endColumn = readNonNegativeNumber(input?.endColumn);
|
|
7702
|
+
const startOffset = readNonNegativeNumber(input?.startOffset);
|
|
7703
|
+
const endOffset = readNonNegativeNumber(input?.endOffset);
|
|
7704
|
+
const path = normalizeOptionalString(input?.path) ?? normalizeOptionalString(fallback.sourcePath);
|
|
7705
|
+
const sourceHash = normalizeOptionalString(input?.sourceHash) ?? normalizeOptionalString(fallback.sourceHash);
|
|
7706
|
+
const expectedSourceHash = normalizeOptionalString(input?.expectedSourceHash) ?? normalizeOptionalString(fallback.expectedSourceHash);
|
|
7707
|
+
return {
|
|
7708
|
+
...(path ? { path } : {}),
|
|
7709
|
+
...(startLine !== undefined ? { startLine: Math.floor(startLine) } : {}),
|
|
7710
|
+
...(startColumn !== undefined ? { startColumn: Math.floor(startColumn) } : {}),
|
|
7711
|
+
...(endLine !== undefined ? { endLine: Math.floor(endLine) } : {}),
|
|
7712
|
+
...(endColumn !== undefined ? { endColumn: Math.floor(endColumn) } : {}),
|
|
7713
|
+
...(startOffset !== undefined ? { startOffset: Math.floor(startOffset) } : {}),
|
|
7714
|
+
...(endOffset !== undefined ? { endOffset: Math.floor(endOffset) } : {}),
|
|
7715
|
+
...(sourceHash ? { sourceHash } : {}),
|
|
7716
|
+
...(expectedSourceHash ? { expectedSourceHash } : {}),
|
|
7717
|
+
...(metadata ? { metadata } : {})
|
|
7718
|
+
};
|
|
7719
|
+
}
|
|
7720
|
+
function deriveSwarmSemanticChangeReasonCodes(input, sourceSpan) {
|
|
7721
|
+
const reasons = [];
|
|
7722
|
+
if (input.conflictReason)
|
|
7723
|
+
reasons.push(input.conflictReason);
|
|
7724
|
+
if (sourceSpan.sourceHash && sourceSpan.expectedSourceHash && sourceSpan.sourceHash !== sourceSpan.expectedSourceHash) {
|
|
7725
|
+
reasons.push('stale-source-hash');
|
|
7726
|
+
}
|
|
7727
|
+
return normalizeSwarmMergeCandidateReasonCodes(reasons);
|
|
7728
|
+
}
|
|
7729
|
+
function deriveSwarmMergeCandidateReasonCodes(input, semanticChanges, sourceSpan) {
|
|
7730
|
+
const reasons = [];
|
|
7731
|
+
if (input.sidecarRequired && !input.sidecarPath)
|
|
7732
|
+
reasons.push('missing-sidecar');
|
|
7733
|
+
if (input.sidecarEmpty)
|
|
7734
|
+
reasons.push('empty-sidecar');
|
|
7735
|
+
if (input.hasLosses)
|
|
7736
|
+
reasons.push('lossy-import');
|
|
7737
|
+
if (input.testsRequired && input.testsPassed !== true)
|
|
7738
|
+
reasons.push('tests-missing');
|
|
7739
|
+
if (input.conflictReason)
|
|
7740
|
+
reasons.push(input.conflictReason);
|
|
7741
|
+
if (sourceSpan.sourceHash && sourceSpan.expectedSourceHash && sourceSpan.sourceHash !== sourceSpan.expectedSourceHash) {
|
|
7742
|
+
reasons.push('stale-source-hash');
|
|
7743
|
+
}
|
|
7744
|
+
if (semanticChanges.length === 0 && input.sidecarPath)
|
|
7745
|
+
reasons.push('empty-sidecar');
|
|
7746
|
+
return normalizeSwarmMergeCandidateReasonCodes(reasons);
|
|
7747
|
+
}
|
|
7748
|
+
function normalizeSwarmSemanticConfidence(value) {
|
|
7749
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
7750
|
+
return clamp01(value);
|
|
7751
|
+
const normalized = normalizeOptionalString(value);
|
|
7752
|
+
return normalized ?? 'medium';
|
|
7753
|
+
}
|
|
7754
|
+
function normalizeSwarmConflictReason(conflictReason, reasonCodes) {
|
|
7755
|
+
const explicit = normalizeOptionalString(conflictReason);
|
|
7756
|
+
if (explicit)
|
|
7757
|
+
return explicit;
|
|
7758
|
+
return reasonCodes.find((reason) => FRONTIER_SWARM_CONFLICT_MERGE_CANDIDATE_REASON_CODES.has(reason));
|
|
7759
|
+
}
|
|
7760
|
+
function normalizeGraphNode(input, generatedAt = Date.now()) {
|
|
7761
|
+
const metadata = toJsonObject(input.metadata);
|
|
7762
|
+
const id = input.id ?? graphNodeId(input.kind, input.title ?? input.jobId ?? input.taskId ?? String(generatedAt));
|
|
7763
|
+
return {
|
|
7764
|
+
id,
|
|
7765
|
+
kind: input.kind,
|
|
7766
|
+
...(input.title ? { title: input.title } : {}),
|
|
7767
|
+
...(input.status ? { status: input.status } : {}),
|
|
7768
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
7769
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
7770
|
+
...(input.path ? { path: input.path } : {}),
|
|
7771
|
+
generatedAt: input.generatedAt ?? generatedAt,
|
|
7772
|
+
...(metadata ? { metadata } : {})
|
|
7773
|
+
};
|
|
7774
|
+
}
|
|
7775
|
+
function normalizeGraphEdge(input, generatedAt = Date.now()) {
|
|
7776
|
+
const metadata = toJsonObject(input.metadata);
|
|
7777
|
+
return {
|
|
7778
|
+
id: input.id ?? `${input.kind}:${input.from}->${input.to}`,
|
|
7779
|
+
from: input.from,
|
|
7780
|
+
to: input.to,
|
|
7781
|
+
kind: input.kind,
|
|
7782
|
+
...(input.label ? { label: input.label } : {}),
|
|
7783
|
+
generatedAt: input.generatedAt ?? generatedAt,
|
|
7784
|
+
...(metadata ? { metadata } : {})
|
|
7785
|
+
};
|
|
7786
|
+
}
|
|
7787
|
+
function normalizeGraphEvent(input, generatedAt = Date.now()) {
|
|
7788
|
+
const timestamp = input.timestamp ?? generatedAt;
|
|
7789
|
+
const metadata = toJsonObject(input.metadata);
|
|
7790
|
+
return {
|
|
7791
|
+
id: input.id ?? 'swarm-graph-event:' + stableHash([input.type, input.nodeId, input.edgeId, timestamp]),
|
|
7792
|
+
type: input.type,
|
|
7793
|
+
...(input.nodeId ? { nodeId: input.nodeId } : {}),
|
|
7794
|
+
...(input.edgeId ? { edgeId: input.edgeId } : {}),
|
|
7795
|
+
timestamp,
|
|
7796
|
+
...(metadata ? { metadata } : {})
|
|
7797
|
+
};
|
|
7798
|
+
}
|
|
7799
|
+
function isPassingGraphStatus(status) {
|
|
7800
|
+
return status === 'passed' || status === 'accepted' || status === 'verified' || status === 'completed';
|
|
7801
|
+
}
|
|
7802
|
+
function isBlockingGraphStatus(status) {
|
|
7803
|
+
return status === 'failed' || status === 'rejected' || status === 'blocked';
|
|
7804
|
+
}
|
|
7805
|
+
function dedupeGraphNodes(nodes) {
|
|
7806
|
+
const byId = new Map();
|
|
7807
|
+
for (const node of nodes)
|
|
7808
|
+
byId.set(node.id, mergeGraphNode(byId.get(node.id), node));
|
|
7809
|
+
return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
7810
|
+
}
|
|
7811
|
+
function mergeGraphNode(existing, node) {
|
|
7812
|
+
if (!existing)
|
|
7813
|
+
return node;
|
|
7814
|
+
return {
|
|
7815
|
+
...existing,
|
|
7816
|
+
...node,
|
|
7817
|
+
metadata: mergeGraphMetadata(existing.metadata, node.metadata)
|
|
7818
|
+
};
|
|
7819
|
+
}
|
|
7820
|
+
function dedupeGraphEdges(edges) {
|
|
7821
|
+
const byId = new Map();
|
|
7822
|
+
for (const edge of edges)
|
|
7823
|
+
byId.set(edge.id, edge);
|
|
7824
|
+
return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
7825
|
+
}
|
|
7826
|
+
function summarizeSwarmRunGraph(nodes, edges, events) {
|
|
7827
|
+
return {
|
|
7828
|
+
nodeCount: nodes.length,
|
|
7829
|
+
edgeCount: edges.length,
|
|
7830
|
+
eventCount: events.length,
|
|
7831
|
+
nodeKinds: countBy(nodes.map((node) => node.kind)),
|
|
7832
|
+
edgeKinds: countBy(edges.map((edge) => edge.kind)),
|
|
7833
|
+
openBlockerCount: nodes.filter((node) => node.status === 'blocked').length + edges.filter((edge) => edge.kind === 'blocks').length,
|
|
7834
|
+
humanQuestionCount: nodes.filter((node) => node.kind === 'decision' && /human|question/i.test(node.title ?? '')).length
|
|
7835
|
+
};
|
|
7836
|
+
}
|
|
7837
|
+
function graphNodeId(kind, value) {
|
|
7838
|
+
return `${kind}:${normalizeGraphIdPart(value)}`;
|
|
7839
|
+
}
|
|
7840
|
+
function normalizeGraphIdPart(value) {
|
|
7841
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '') || stableHash(value);
|
|
7842
|
+
}
|
|
7843
|
+
function normalizeSwarmGraphRef(input) {
|
|
7844
|
+
return {
|
|
7845
|
+
id: input.id ?? 'swarm-graph-ref:' + stableHash([input.kind, input.graphId, input.nodeId, input.edgeId, input.role]),
|
|
7846
|
+
kind: input.kind ?? 'graph',
|
|
7847
|
+
...(input.graphId ? { graphId: input.graphId } : {}),
|
|
7848
|
+
...(input.nodeId ? { nodeId: input.nodeId } : {}),
|
|
7849
|
+
...(input.edgeId ? { edgeId: input.edgeId } : {}),
|
|
7850
|
+
...(input.role ? { role: input.role } : {}),
|
|
7851
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
7852
|
+
};
|
|
7853
|
+
}
|
|
7854
|
+
function normalizeSwarmGraphRefs(input) {
|
|
7855
|
+
return input.map(normalizeSwarmGraphRef).sort((left, right) => left.id.localeCompare(right.id));
|
|
7856
|
+
}
|
|
7857
|
+
function normalizeSwarmPatchEvent(input, replayRecordId, index) {
|
|
7858
|
+
if (isSwarmPatchEvent(input) && (!replayRecordId || input.replayRecordId) && (index === undefined || input.sequence !== undefined)) {
|
|
7859
|
+
return input;
|
|
7860
|
+
}
|
|
7861
|
+
return createSwarmPatchEvent({
|
|
7862
|
+
...input,
|
|
7863
|
+
replayRecordId: input.replayRecordId ?? replayRecordId,
|
|
7864
|
+
sequence: input.sequence ?? index
|
|
7865
|
+
});
|
|
7866
|
+
}
|
|
7867
|
+
function inferImprovementLoopStatus(input) {
|
|
7868
|
+
if (input.result !== undefined)
|
|
7869
|
+
return 'applied';
|
|
7870
|
+
if (input.action !== undefined)
|
|
7871
|
+
return 'planned';
|
|
7872
|
+
if (input.observation !== undefined)
|
|
7873
|
+
return 'observed';
|
|
7874
|
+
return 'unknown';
|
|
7875
|
+
}
|
|
7876
|
+
function createImprovementLoopPhaseNode(loop, phase, value, generatedAt) {
|
|
7877
|
+
return {
|
|
7878
|
+
id: graphNodeId('rsi', `${loop.id}:${phase}`),
|
|
7879
|
+
kind: 'rsi',
|
|
7880
|
+
title: `${loop.title} ${phase}`,
|
|
7881
|
+
status: loop.status,
|
|
7882
|
+
jobId: loop.jobId,
|
|
7883
|
+
taskId: loop.taskId,
|
|
7884
|
+
generatedAt,
|
|
7885
|
+
metadata: {
|
|
7886
|
+
phase,
|
|
7887
|
+
value,
|
|
7888
|
+
loop
|
|
7889
|
+
}
|
|
7890
|
+
};
|
|
7891
|
+
}
|
|
7892
|
+
function isSwarmGateRecord(input) {
|
|
7893
|
+
return input.kind === FRONTIER_SWARM_GATE_RECORD_KIND;
|
|
7894
|
+
}
|
|
7895
|
+
function isSwarmEvidenceRecord(input) {
|
|
7896
|
+
return input.kind === FRONTIER_SWARM_EVIDENCE_RECORD_KIND;
|
|
7897
|
+
}
|
|
7898
|
+
function isSwarmPatchEvent(input) {
|
|
7899
|
+
return input.kind === FRONTIER_SWARM_PATCH_EVENT_KIND;
|
|
7900
|
+
}
|
|
7901
|
+
function isSwarmReplayRecord(input) {
|
|
7902
|
+
return input.kind === FRONTIER_SWARM_REPLAY_RECORD_KIND;
|
|
7903
|
+
}
|
|
7904
|
+
function isSwarmImprovementLoop(input) {
|
|
7905
|
+
return input.kind === FRONTIER_SWARM_IMPROVEMENT_LOOP_KIND;
|
|
7906
|
+
}
|
|
7907
|
+
function mergeGraphMetadata(left, right) {
|
|
7908
|
+
if (!left)
|
|
7909
|
+
return right;
|
|
7910
|
+
if (!right)
|
|
7911
|
+
return left;
|
|
7912
|
+
return { ...left, ...right };
|
|
7913
|
+
}
|
|
5870
7914
|
function hasJobDependencyCycle(start, dependenciesByJobId) {
|
|
5871
7915
|
const visiting = new Set();
|
|
5872
7916
|
const visited = new Set();
|
|
@@ -8123,6 +10167,141 @@ function joinPathParts(...parts) {
|
|
|
8123
10167
|
.filter(Boolean)
|
|
8124
10168
|
.join('/');
|
|
8125
10169
|
}
|
|
10170
|
+
function swarmRunEventTime(options, fallback) {
|
|
10171
|
+
if (options.time)
|
|
10172
|
+
return options.time;
|
|
10173
|
+
const value = options.now ?? fallback;
|
|
10174
|
+
return new Date(Number.isFinite(value) ? value : Date.now()).toISOString();
|
|
10175
|
+
}
|
|
10176
|
+
function isFrontierRunEventList(input) {
|
|
10177
|
+
return Array.isArray(input);
|
|
10178
|
+
}
|
|
10179
|
+
function swarmRunLaneNodeId(lane) {
|
|
10180
|
+
return String(lane).startsWith('lane:') ? String(lane) : `lane:${lane}`;
|
|
10181
|
+
}
|
|
10182
|
+
function swarmRunTaskNodeId(taskId) {
|
|
10183
|
+
return String(taskId).startsWith('task:') ? String(taskId) : `task:${taskId}`;
|
|
10184
|
+
}
|
|
10185
|
+
function swarmRunAttemptNodeId(jobId) {
|
|
10186
|
+
return String(jobId).startsWith('attempt:') ? String(jobId) : `attempt:${jobId}`;
|
|
10187
|
+
}
|
|
10188
|
+
function swarmRunLeaseNodeId(leaseId) {
|
|
10189
|
+
return String(leaseId).startsWith('lease:') ? String(leaseId) : `lease:${leaseId}`;
|
|
10190
|
+
}
|
|
10191
|
+
function swarmRunPatchNodeId(jobId) {
|
|
10192
|
+
return String(jobId).startsWith('patch:') ? String(jobId) : `patch:${jobId}`;
|
|
10193
|
+
}
|
|
10194
|
+
function swarmRunArtifactNodeId(artifactPath) {
|
|
10195
|
+
return `artifact:${slug(artifactPath)}:${stableHash(artifactPath).slice('fnv1a32:'.length)}`;
|
|
10196
|
+
}
|
|
10197
|
+
function swarmRunEvidenceNodeId(jobId, evidencePath) {
|
|
10198
|
+
return `evidence:${slug(jobId)}:${stableHash(evidencePath).slice('fnv1a32:'.length)}`;
|
|
10199
|
+
}
|
|
10200
|
+
function swarmRunVerificationNodeId(jobId, index, name) {
|
|
10201
|
+
return `verification:${slug(jobId)}:${index}:${slug(name || 'command')}`;
|
|
10202
|
+
}
|
|
10203
|
+
function swarmRunDecisionNodeId(id) {
|
|
10204
|
+
return String(id).startsWith('decision:') ? String(id) : `decision:${id}`;
|
|
10205
|
+
}
|
|
10206
|
+
function inferSwarmLanePackageId(jobs) {
|
|
10207
|
+
for (const job of jobs) {
|
|
10208
|
+
const candidate = job.task.metadata?.packageId ?? job.metadata?.packageId ?? job.task.metadata?.package ?? job.metadata?.package;
|
|
10209
|
+
if (typeof candidate === 'string' && candidate)
|
|
10210
|
+
return candidate;
|
|
10211
|
+
}
|
|
10212
|
+
return undefined;
|
|
10213
|
+
}
|
|
10214
|
+
function formatSwarmCommandLine(command) {
|
|
10215
|
+
return [command.command, ...command.args].join(' ');
|
|
10216
|
+
}
|
|
10217
|
+
function swarmStatusToRunStatus(status) {
|
|
10218
|
+
if (status === 'completed' || status === 'verified')
|
|
10219
|
+
return 'done';
|
|
10220
|
+
if (status === 'failed' || status === 'rejected')
|
|
10221
|
+
return 'rejected';
|
|
10222
|
+
if (status === 'blocked')
|
|
10223
|
+
return 'blocked';
|
|
10224
|
+
if (status === 'running')
|
|
10225
|
+
return 'running';
|
|
10226
|
+
if (status === 'planned')
|
|
10227
|
+
return 'planned';
|
|
10228
|
+
return 'ready';
|
|
10229
|
+
}
|
|
10230
|
+
function swarmResultStatusToAttemptStatus(status) {
|
|
10231
|
+
if (status === 'completed' || status === 'verified')
|
|
10232
|
+
return 'completed';
|
|
10233
|
+
if (status === 'running')
|
|
10234
|
+
return 'running';
|
|
10235
|
+
if (status === 'cancelled')
|
|
10236
|
+
return 'cancelled';
|
|
10237
|
+
if (status === 'timed-out' || status === 'timeout')
|
|
10238
|
+
return 'timed-out';
|
|
10239
|
+
if (status === 'queued' || status === 'ready' || status === 'planned')
|
|
10240
|
+
return 'queued';
|
|
10241
|
+
return 'failed';
|
|
10242
|
+
}
|
|
10243
|
+
function swarmRiskToRunRisk(risk) {
|
|
10244
|
+
if (risk === 'low' || risk === 'medium' || risk === 'high')
|
|
10245
|
+
return risk;
|
|
10246
|
+
return 'medium';
|
|
10247
|
+
}
|
|
10248
|
+
function inferSwarmArtifactType(file) {
|
|
10249
|
+
const lower = file.toLowerCase();
|
|
10250
|
+
if (lower.endsWith('.patch') || lower.endsWith('.diff'))
|
|
10251
|
+
return 'patch';
|
|
10252
|
+
if (lower.endsWith('.json') || lower.endsWith('.jsonl'))
|
|
10253
|
+
return 'json';
|
|
10254
|
+
if (lower.endsWith('.log') || lower.endsWith('.txt') || lower.includes('stdout') || lower.includes('stderr'))
|
|
10255
|
+
return 'log';
|
|
10256
|
+
if (lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg') || lower.endsWith('.webp'))
|
|
10257
|
+
return 'image';
|
|
10258
|
+
return 'artifact';
|
|
10259
|
+
}
|
|
10260
|
+
function swarmDispositionToRunDecision(disposition, readiness) {
|
|
10261
|
+
if (disposition === 'auto-mergeable' || readiness === 'verified-patch')
|
|
10262
|
+
return 'apply';
|
|
10263
|
+
if (disposition === 'stale-against-head')
|
|
10264
|
+
return 'rerun';
|
|
10265
|
+
if (disposition === 'rejected' || readiness === 'rejected')
|
|
10266
|
+
return 'reject';
|
|
10267
|
+
if (disposition === 'needs-port' || disposition === 'blocked' || readiness === 'blocked')
|
|
10268
|
+
return 'human-question';
|
|
10269
|
+
return 'record-only';
|
|
10270
|
+
}
|
|
10271
|
+
function swarmQueueOutcomeToRunDecision(decision) {
|
|
10272
|
+
if (decision.outcome === 'applied' || decision.decision === 'applied' || decision.action === 'apply-local')
|
|
10273
|
+
return 'apply';
|
|
10274
|
+
if (decision.outcome === 'rerun' || decision.decision === 'rerun' || decision.staleOrRerun)
|
|
10275
|
+
return 'rerun';
|
|
10276
|
+
if (decision.outcome === 'rejected' || decision.decision === 'rejected')
|
|
10277
|
+
return 'reject';
|
|
10278
|
+
if (decision.action === 'promote')
|
|
10279
|
+
return 'promote';
|
|
10280
|
+
if (decision.humanBlocked || decision.outcome === 'human-blocked')
|
|
10281
|
+
return 'human-question';
|
|
10282
|
+
return 'record-only';
|
|
10283
|
+
}
|
|
10284
|
+
function swarmQueueOutcomeRequiredActions(decision) {
|
|
10285
|
+
const actions = [];
|
|
10286
|
+
if (decision.coordinatorReview || decision.reviewDebt)
|
|
10287
|
+
actions.push('coordinator-review');
|
|
10288
|
+
if (decision.humanBlocked)
|
|
10289
|
+
actions.push('human-answer');
|
|
10290
|
+
if (decision.staleOrRerun)
|
|
10291
|
+
actions.push('rerun');
|
|
10292
|
+
if (decision.conflict)
|
|
10293
|
+
actions.push('resolve-conflict');
|
|
10294
|
+
return uniqueStrings(actions);
|
|
10295
|
+
}
|
|
10296
|
+
function pruneUndefinedJsonObject(value) {
|
|
10297
|
+
const out = {};
|
|
10298
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
10299
|
+
if (entry === undefined)
|
|
10300
|
+
continue;
|
|
10301
|
+
out[key] = toJsonValue(entry);
|
|
10302
|
+
}
|
|
10303
|
+
return out;
|
|
10304
|
+
}
|
|
8126
10305
|
function normalizeId(value, label) {
|
|
8127
10306
|
const id = String(value || '').trim();
|
|
8128
10307
|
if (!id)
|