@shapeshift-labs/frontier-swarm 0.5.21 → 0.5.23
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 +778 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2482 -24
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createRunDashboardSnapshot, createRunEdgeEvent, createRunEvent, createRunNodeEvent, defineRunArtifact, defineRunAttempt, defineRunDecision, defineRunEvidence, defineRunLane, defineRunPatch, defineRunTask, defineRunVerification, linkRunNodes, replayRunEvents } from '@shapeshift-labs/frontier-run';
|
|
2
|
+
import { acquireSemanticLease, createSemanticLeaseState, defineSemanticLeaseScope, validateSemanticLeaseFence } from '@shapeshift-labs/frontier-lease';
|
|
1
3
|
export const FRONTIER_SWARM_MANIFEST_KIND = 'frontier.swarm.manifest';
|
|
2
4
|
export const FRONTIER_SWARM_MANIFEST_VERSION = 1;
|
|
3
5
|
export const FRONTIER_SWARM_TASK_KIND = 'frontier.swarm.task';
|
|
@@ -10,6 +12,22 @@ export const FRONTIER_SWARM_EVENT_KIND = 'frontier.swarm.event';
|
|
|
10
12
|
export const FRONTIER_SWARM_EVENT_VERSION = 1;
|
|
11
13
|
export const FRONTIER_SWARM_EVENT_STREAM_KIND = 'frontier.swarm.event-stream';
|
|
12
14
|
export const FRONTIER_SWARM_EVENT_STREAM_VERSION = 1;
|
|
15
|
+
export const FRONTIER_SWARM_RUN_GRAPH_KIND = 'frontier.swarm.run-graph';
|
|
16
|
+
export const FRONTIER_SWARM_RUN_GRAPH_VERSION = 1;
|
|
17
|
+
export const FRONTIER_SWARM_GRAPH_SNAPSHOT_KIND = 'frontier.swarm.graph-snapshot';
|
|
18
|
+
export const FRONTIER_SWARM_GRAPH_SNAPSHOT_VERSION = 1;
|
|
19
|
+
export const FRONTIER_SWARM_RUN_GRAPH_CHUNK_KIND = 'frontier.swarm.run-graph-chunk';
|
|
20
|
+
export const FRONTIER_SWARM_RUN_GRAPH_CHUNK_VERSION = 1;
|
|
21
|
+
export const FRONTIER_SWARM_GATE_RECORD_KIND = 'frontier.swarm.gate-record';
|
|
22
|
+
export const FRONTIER_SWARM_GATE_RECORD_VERSION = 1;
|
|
23
|
+
export const FRONTIER_SWARM_EVIDENCE_RECORD_KIND = 'frontier.swarm.evidence-record';
|
|
24
|
+
export const FRONTIER_SWARM_EVIDENCE_RECORD_VERSION = 1;
|
|
25
|
+
export const FRONTIER_SWARM_PATCH_EVENT_KIND = 'frontier.swarm.patch-event';
|
|
26
|
+
export const FRONTIER_SWARM_PATCH_EVENT_VERSION = 1;
|
|
27
|
+
export const FRONTIER_SWARM_REPLAY_RECORD_KIND = 'frontier.swarm.replay-record';
|
|
28
|
+
export const FRONTIER_SWARM_REPLAY_RECORD_VERSION = 1;
|
|
29
|
+
export const FRONTIER_SWARM_IMPROVEMENT_LOOP_KIND = 'frontier.swarm.improvement-loop';
|
|
30
|
+
export const FRONTIER_SWARM_IMPROVEMENT_LOOP_VERSION = 1;
|
|
13
31
|
export const FRONTIER_SWARM_MAILBOX_KIND = 'frontier.swarm.mailbox';
|
|
14
32
|
export const FRONTIER_SWARM_MAILBOX_VERSION = 1;
|
|
15
33
|
export const FRONTIER_SWARM_PROOF_KIND = 'frontier.swarm.proof';
|
|
@@ -30,6 +48,10 @@ export const FRONTIER_SWARM_MERGE_PLAN_KIND = 'frontier.swarm.merge-plan';
|
|
|
30
48
|
export const FRONTIER_SWARM_MERGE_PLAN_VERSION = 1;
|
|
31
49
|
export const FRONTIER_SWARM_MERGE_BUNDLE_KIND = 'frontier.swarm.merge-bundle';
|
|
32
50
|
export const FRONTIER_SWARM_MERGE_BUNDLE_VERSION = 1;
|
|
51
|
+
export const FRONTIER_SWARM_SEMANTIC_CHANGE_KIND = 'frontier.swarm.semantic-change';
|
|
52
|
+
export const FRONTIER_SWARM_SEMANTIC_CHANGE_VERSION = 1;
|
|
53
|
+
export const FRONTIER_SWARM_MERGE_CANDIDATE_KIND = 'frontier.swarm.merge-candidate';
|
|
54
|
+
export const FRONTIER_SWARM_MERGE_CANDIDATE_VERSION = 1;
|
|
33
55
|
export const FRONTIER_SWARM_QUEUE_OVERLAY_KIND = 'frontier.swarm.queue-overlay';
|
|
34
56
|
export const FRONTIER_SWARM_QUEUE_OVERLAY_VERSION = 1;
|
|
35
57
|
export const FRONTIER_SWARM_MERGE_INDEX_KIND = 'frontier.swarm.merge-index';
|
|
@@ -96,6 +118,8 @@ export const FRONTIER_SWARM_QUEUE_OUTCOME_MODEL_KIND = 'frontier.swarm.queue-out
|
|
|
96
118
|
export const FRONTIER_SWARM_QUEUE_OUTCOME_MODEL_VERSION = 1;
|
|
97
119
|
export const FRONTIER_SWARM_TERMINAL_STATE_RECONCILIATION_KIND = 'frontier.swarm.terminal-state-reconciliation';
|
|
98
120
|
export const FRONTIER_SWARM_TERMINAL_STATE_RECONCILIATION_VERSION = 1;
|
|
121
|
+
export const FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_KIND = 'frontier.swarm.terminal-outcome-record';
|
|
122
|
+
export const FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_VERSION = 1;
|
|
99
123
|
export const FRONTIER_SWARM_PRIORITY_POLICY_KIND = 'frontier.swarm.priority-policy';
|
|
100
124
|
export const FRONTIER_SWARM_PRIORITY_POLICY_VERSION = 1;
|
|
101
125
|
export const FRONTIER_SWARM_MODEL_ROUTE_KIND = 'frontier.swarm.model-route';
|
|
@@ -118,6 +142,42 @@ export const FRONTIER_SWARM_VERIFICATION_CATEGORY_HINTS = [
|
|
|
118
142
|
'browser',
|
|
119
143
|
'oracle'
|
|
120
144
|
];
|
|
145
|
+
export const FRONTIER_SWARM_GRAPH_NODE_KINDS = [
|
|
146
|
+
'intent',
|
|
147
|
+
'task',
|
|
148
|
+
'worker',
|
|
149
|
+
'candidate',
|
|
150
|
+
'evidence',
|
|
151
|
+
'gate',
|
|
152
|
+
'decision',
|
|
153
|
+
'merge',
|
|
154
|
+
'replay',
|
|
155
|
+
'rsi'
|
|
156
|
+
];
|
|
157
|
+
export const FRONTIER_SWARM_GRAPH_EDGE_KINDS = [
|
|
158
|
+
'dependsOn',
|
|
159
|
+
'blocks',
|
|
160
|
+
'produces',
|
|
161
|
+
'verifies',
|
|
162
|
+
'conflictsWith',
|
|
163
|
+
'supersedes',
|
|
164
|
+
'mergesInto'
|
|
165
|
+
];
|
|
166
|
+
export const FRONTIER_SWARM_MERGE_CANDIDATE_ADMISSION_STATUSES = [
|
|
167
|
+
'safe',
|
|
168
|
+
'safe-with-losses',
|
|
169
|
+
'review-required',
|
|
170
|
+
'blocked'
|
|
171
|
+
];
|
|
172
|
+
export const FRONTIER_SWARM_MERGE_CANDIDATE_REASON_CODES = [
|
|
173
|
+
'missing-sidecar',
|
|
174
|
+
'empty-sidecar',
|
|
175
|
+
'stale-source-hash',
|
|
176
|
+
'symbol-conflict',
|
|
177
|
+
'effect-conflict',
|
|
178
|
+
'lossy-import',
|
|
179
|
+
'tests-missing'
|
|
180
|
+
];
|
|
121
181
|
export const FRONTIER_SWARM_SEMANTIC_OWNERSHIP_EXPORT_STABLE_KEY_KIND = 'exported-declaration';
|
|
122
182
|
export const FRONTIER_SWARM_SEMANTIC_OWNERSHIP_NAMESPACE_EXPORT_STABLE_KEY_KIND = 'namespace-export';
|
|
123
183
|
export const FRONTIER_SWARM_SEMANTIC_OWNERSHIP_TYPE_STABLE_KEY_KIND = 'type-declaration';
|
|
@@ -176,6 +236,7 @@ export const FRONTIER_SWARM_TERMINAL_OUTCOME_LABELS = [
|
|
|
176
236
|
'superseded',
|
|
177
237
|
'evidence-only',
|
|
178
238
|
'no-change',
|
|
239
|
+
'research-complete',
|
|
179
240
|
'generated-by-collector',
|
|
180
241
|
'patch-missing',
|
|
181
242
|
'bundle-missing',
|
|
@@ -186,6 +247,26 @@ export const FRONTIER_SWARM_TERMINAL_OUTCOME_LABELS = [
|
|
|
186
247
|
'human-blocked',
|
|
187
248
|
'coordinator-review'
|
|
188
249
|
];
|
|
250
|
+
export const FRONTIER_SWARM_TERMINAL_OUTCOME_STATUSES = [
|
|
251
|
+
'applied',
|
|
252
|
+
'rejected',
|
|
253
|
+
'superseded',
|
|
254
|
+
'no-change',
|
|
255
|
+
'conflict',
|
|
256
|
+
'human-needed',
|
|
257
|
+
'research-complete',
|
|
258
|
+
'rerun'
|
|
259
|
+
];
|
|
260
|
+
export const FRONTIER_SWARM_TERMINAL_OUTCOME_REASON_CODES = [
|
|
261
|
+
'accepted-by-admission',
|
|
262
|
+
'failed-verification',
|
|
263
|
+
'superseded-by-newer-output',
|
|
264
|
+
'no-effective-change',
|
|
265
|
+
'conflict-detected',
|
|
266
|
+
'human-decision-required',
|
|
267
|
+
'research-complete',
|
|
268
|
+
'stale-rerun-required'
|
|
269
|
+
];
|
|
189
270
|
const DEFAULT_COMPLETED_STATUSES = ['completed', 'verified', 'done', 'verified-local-harness'];
|
|
190
271
|
const DEFAULT_SWARM_EVENT_TYPES = [
|
|
191
272
|
'swarm.started',
|
|
@@ -378,6 +459,1016 @@ export function createSwarmTaskSelection(manifestInput, taskInput, options = {})
|
|
|
378
459
|
summary: summarizeTaskSelection(entries)
|
|
379
460
|
};
|
|
380
461
|
}
|
|
462
|
+
export function createSwarmRunGraph(input = {}) {
|
|
463
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
464
|
+
const nodes = dedupeGraphNodes((input.nodes ?? []).map((node) => normalizeGraphNode(node, generatedAt)));
|
|
465
|
+
const knownNodeIds = new Set(nodes.map((node) => node.id));
|
|
466
|
+
const edges = dedupeGraphEdges((input.edges ?? [])
|
|
467
|
+
.map((edge) => normalizeGraphEdge(edge, generatedAt))
|
|
468
|
+
.filter((edge) => knownNodeIds.has(edge.from) && knownNodeIds.has(edge.to)));
|
|
469
|
+
const knownEdgeIds = new Set(edges.map((edge) => edge.id));
|
|
470
|
+
const events = (input.events ?? [])
|
|
471
|
+
.map((event) => normalizeGraphEvent(event, generatedAt))
|
|
472
|
+
.filter((event) => event.nodeId === undefined || knownNodeIds.has(event.nodeId))
|
|
473
|
+
.filter((event) => event.edgeId === undefined || knownEdgeIds.has(event.edgeId))
|
|
474
|
+
.sort((left, right) => left.timestamp - right.timestamp || left.id.localeCompare(right.id));
|
|
475
|
+
const graph = {
|
|
476
|
+
kind: FRONTIER_SWARM_RUN_GRAPH_KIND,
|
|
477
|
+
version: FRONTIER_SWARM_RUN_GRAPH_VERSION,
|
|
478
|
+
id: input.id ?? 'swarm-run-graph:' + stableHash([input.runId, nodes.map((node) => node.id), edges.map((edge) => edge.id)]),
|
|
479
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
480
|
+
...(input.title ? { title: input.title } : {}),
|
|
481
|
+
generatedAt,
|
|
482
|
+
nodes,
|
|
483
|
+
edges,
|
|
484
|
+
events,
|
|
485
|
+
summary: summarizeSwarmRunGraph(nodes, edges, events),
|
|
486
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
487
|
+
};
|
|
488
|
+
return graph;
|
|
489
|
+
}
|
|
490
|
+
export function normalizeSwarmRunGraph(input) {
|
|
491
|
+
if (input.kind === FRONTIER_SWARM_RUN_GRAPH_KIND) {
|
|
492
|
+
const graph = input;
|
|
493
|
+
return createSwarmRunGraph({
|
|
494
|
+
id: graph.id,
|
|
495
|
+
runId: graph.runId,
|
|
496
|
+
title: graph.title,
|
|
497
|
+
generatedAt: graph.generatedAt,
|
|
498
|
+
nodes: graph.nodes,
|
|
499
|
+
edges: graph.edges,
|
|
500
|
+
events: graph.events,
|
|
501
|
+
metadata: graph.metadata
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return createSwarmRunGraph(input);
|
|
505
|
+
}
|
|
506
|
+
export function createSwarmGraphSnapshot(input) {
|
|
507
|
+
const graph = normalizeSwarmRunGraph(input.graph);
|
|
508
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
509
|
+
return {
|
|
510
|
+
kind: FRONTIER_SWARM_GRAPH_SNAPSHOT_KIND,
|
|
511
|
+
version: FRONTIER_SWARM_GRAPH_SNAPSHOT_VERSION,
|
|
512
|
+
id: input.id ?? 'swarm-graph-snapshot:' + stableHash([graph.id, generatedAt, graph.summary]),
|
|
513
|
+
graphId: graph.id,
|
|
514
|
+
generatedAt,
|
|
515
|
+
summary: graph.summary,
|
|
516
|
+
graph,
|
|
517
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
export function createSwarmGateRecord(input) {
|
|
521
|
+
const metrics = toJsonObject(input.metrics);
|
|
522
|
+
const metadata = toJsonObject(input.metadata);
|
|
523
|
+
return {
|
|
524
|
+
kind: FRONTIER_SWARM_GATE_RECORD_KIND,
|
|
525
|
+
version: FRONTIER_SWARM_GATE_RECORD_VERSION,
|
|
526
|
+
id: input.id ?? 'swarm-gate:' + stableHash([input.type, input.command, input.path, input.jobId, input.taskId, input.startedAt, input.finishedAt]),
|
|
527
|
+
type: input.type,
|
|
528
|
+
status: input.status ?? 'unknown',
|
|
529
|
+
required: input.required ?? false,
|
|
530
|
+
...(input.command ? { command: input.command } : {}),
|
|
531
|
+
...(input.path ? { path: input.path } : {}),
|
|
532
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
533
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
534
|
+
...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}),
|
|
535
|
+
...(input.finishedAt !== undefined ? { finishedAt: input.finishedAt } : {}),
|
|
536
|
+
...(input.failureReason ? { failureReason: input.failureReason } : {}),
|
|
537
|
+
...(input.confidence ? { confidence: input.confidence } : {}),
|
|
538
|
+
...(metrics ? { metrics } : {}),
|
|
539
|
+
...(metadata ? { metadata } : {})
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
export function createSwarmEvidenceRecord(input) {
|
|
543
|
+
const metrics = toJsonObject(input.metrics);
|
|
544
|
+
const metadata = toJsonObject(input.metadata);
|
|
545
|
+
const producedAt = input.producedAt ?? Date.now();
|
|
546
|
+
return {
|
|
547
|
+
kind: FRONTIER_SWARM_EVIDENCE_RECORD_KIND,
|
|
548
|
+
version: FRONTIER_SWARM_EVIDENCE_RECORD_VERSION,
|
|
549
|
+
id: input.id ?? 'swarm-evidence:' + stableHash([input.type, input.path, input.ref, input.jobId, input.taskId, input.gateId]),
|
|
550
|
+
type: input.type,
|
|
551
|
+
...(input.path ? { path: input.path } : {}),
|
|
552
|
+
...(input.ref ? { ref: input.ref } : {}),
|
|
553
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
554
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
555
|
+
...(input.gateId ? { gateId: input.gateId } : {}),
|
|
556
|
+
producedAt,
|
|
557
|
+
...(input.confidence ? { confidence: input.confidence } : {}),
|
|
558
|
+
...(metrics ? { metrics } : {}),
|
|
559
|
+
...(metadata ? { metadata } : {})
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
export function createSwarmPatchEvent(input) {
|
|
563
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
564
|
+
const graphRefs = normalizeSwarmGraphRefs(input.graphRefs ?? []);
|
|
565
|
+
const sequence = input.sequence !== undefined ? Math.max(0, Math.floor(input.sequence)) : undefined;
|
|
566
|
+
return {
|
|
567
|
+
kind: FRONTIER_SWARM_PATCH_EVENT_KIND,
|
|
568
|
+
version: FRONTIER_SWARM_PATCH_EVENT_VERSION,
|
|
569
|
+
id: input.id ?? 'swarm-patch-event:' + stableHash([input.replayRecordId, sequence, input.operation, input.path, input.from, input.hash]),
|
|
570
|
+
operation: input.operation,
|
|
571
|
+
path: input.path,
|
|
572
|
+
...(input.from ? { from: input.from } : {}),
|
|
573
|
+
...(input.hash ? { hash: input.hash } : {}),
|
|
574
|
+
...(sequence !== undefined ? { sequence } : {}),
|
|
575
|
+
...(input.value !== undefined ? { value: toJsonValue(input.value) } : {}),
|
|
576
|
+
...(input.before !== undefined ? { before: toJsonValue(input.before) } : {}),
|
|
577
|
+
...(input.after !== undefined ? { after: toJsonValue(input.after) } : {}),
|
|
578
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
579
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
580
|
+
...(input.replayRecordId ? { replayRecordId: input.replayRecordId } : {}),
|
|
581
|
+
graphRefs,
|
|
582
|
+
generatedAt,
|
|
583
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
export function createSwarmReplayRecord(input = {}) {
|
|
587
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
588
|
+
const title = input.title ?? titleFromId(input.id ?? input.subject ?? input.replayBundleId ?? 'replay record');
|
|
589
|
+
const evidenceRefs = normalizeNamedRefs(input.evidenceRefs ?? [], 'evidence');
|
|
590
|
+
const graphRefs = normalizeSwarmGraphRefs(input.graphRefs ?? []);
|
|
591
|
+
const replayId = input.id ?? 'swarm-replay-record:' + stableHash([
|
|
592
|
+
title,
|
|
593
|
+
input.status,
|
|
594
|
+
input.subject,
|
|
595
|
+
input.replayBundleId,
|
|
596
|
+
input.patchEvents ?? [],
|
|
597
|
+
evidenceRefs,
|
|
598
|
+
graphRefs,
|
|
599
|
+
input.jobId,
|
|
600
|
+
input.taskId
|
|
601
|
+
]);
|
|
602
|
+
const patchEvents = (input.patchEvents ?? [])
|
|
603
|
+
.map((event, index) => normalizeSwarmPatchEvent(event, replayId, index))
|
|
604
|
+
.sort((left, right) => (left.sequence ?? Number.MAX_SAFE_INTEGER) - (right.sequence ?? Number.MAX_SAFE_INTEGER) || left.id.localeCompare(right.id));
|
|
605
|
+
return {
|
|
606
|
+
kind: FRONTIER_SWARM_REPLAY_RECORD_KIND,
|
|
607
|
+
version: FRONTIER_SWARM_REPLAY_RECORD_VERSION,
|
|
608
|
+
id: replayId,
|
|
609
|
+
title,
|
|
610
|
+
status: input.status ?? 'unknown',
|
|
611
|
+
...(input.subject ? { subject: input.subject } : {}),
|
|
612
|
+
...(input.replayBundleId ? { replayBundleId: input.replayBundleId } : {}),
|
|
613
|
+
patchEvents,
|
|
614
|
+
evidenceRefs,
|
|
615
|
+
graphRefs,
|
|
616
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
617
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
618
|
+
...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}),
|
|
619
|
+
...(input.finishedAt !== undefined ? { finishedAt: input.finishedAt } : {}),
|
|
620
|
+
generatedAt,
|
|
621
|
+
summary: {
|
|
622
|
+
patchEventCount: patchEvents.length,
|
|
623
|
+
evidenceRefCount: evidenceRefs.length,
|
|
624
|
+
graphRefCount: graphRefs.length
|
|
625
|
+
},
|
|
626
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
export function createSwarmImprovementLoop(input = {}) {
|
|
630
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
631
|
+
const title = input.title ?? titleFromId(input.id ?? input.subject ?? 'improvement loop');
|
|
632
|
+
const replayRecordIds = uniqueStrings(input.replayRecordIds ?? []);
|
|
633
|
+
const patchEventIds = uniqueStrings(input.patchEventIds ?? []);
|
|
634
|
+
const evidenceRefs = normalizeNamedRefs(input.evidenceRefs ?? [], 'evidence');
|
|
635
|
+
const graphRefs = normalizeSwarmGraphRefs(input.graphRefs ?? []);
|
|
636
|
+
return {
|
|
637
|
+
kind: FRONTIER_SWARM_IMPROVEMENT_LOOP_KIND,
|
|
638
|
+
version: FRONTIER_SWARM_IMPROVEMENT_LOOP_VERSION,
|
|
639
|
+
id: input.id ?? 'swarm-improvement-loop:' + stableHash([
|
|
640
|
+
title,
|
|
641
|
+
input.status,
|
|
642
|
+
input.subject,
|
|
643
|
+
input.observation,
|
|
644
|
+
input.action,
|
|
645
|
+
input.result,
|
|
646
|
+
replayRecordIds,
|
|
647
|
+
patchEventIds,
|
|
648
|
+
evidenceRefs,
|
|
649
|
+
graphRefs,
|
|
650
|
+
input.jobId,
|
|
651
|
+
input.taskId
|
|
652
|
+
]),
|
|
653
|
+
title,
|
|
654
|
+
status: input.status ?? inferImprovementLoopStatus(input),
|
|
655
|
+
...(input.subject ? { subject: input.subject } : {}),
|
|
656
|
+
...(input.observation !== undefined ? { observation: toJsonValue(input.observation) } : {}),
|
|
657
|
+
...(input.action !== undefined ? { action: toJsonValue(input.action) } : {}),
|
|
658
|
+
...(input.result !== undefined ? { result: toJsonValue(input.result) } : {}),
|
|
659
|
+
replayRecordIds,
|
|
660
|
+
patchEventIds,
|
|
661
|
+
evidenceRefs,
|
|
662
|
+
graphRefs,
|
|
663
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
664
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
665
|
+
generatedAt,
|
|
666
|
+
summary: {
|
|
667
|
+
replayRecordCount: replayRecordIds.length,
|
|
668
|
+
patchEventCount: patchEventIds.length,
|
|
669
|
+
evidenceRefCount: evidenceRefs.length,
|
|
670
|
+
graphRefCount: graphRefs.length
|
|
671
|
+
},
|
|
672
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
export function mapSwarmGateEvidenceToGraph(input) {
|
|
676
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
677
|
+
const gateRecords = (input.gates ?? []).map((gate) => isSwarmGateRecord(gate) ? gate : createSwarmGateRecord(gate));
|
|
678
|
+
const evidenceRecords = (input.evidence ?? []).map((evidence) => isSwarmEvidenceRecord(evidence) ? evidence : createSwarmEvidenceRecord(evidence));
|
|
679
|
+
const nodes = [
|
|
680
|
+
...gateRecords.map((gate) => ({
|
|
681
|
+
id: graphNodeId('gate', gate.id),
|
|
682
|
+
kind: 'gate',
|
|
683
|
+
title: gate.command ?? gate.path ?? gate.type,
|
|
684
|
+
status: gate.status,
|
|
685
|
+
jobId: gate.jobId,
|
|
686
|
+
taskId: gate.taskId,
|
|
687
|
+
path: gate.path,
|
|
688
|
+
generatedAt,
|
|
689
|
+
metadata: gate
|
|
690
|
+
})),
|
|
691
|
+
...evidenceRecords.map((evidence) => ({
|
|
692
|
+
id: graphNodeId('evidence', evidence.id),
|
|
693
|
+
kind: 'evidence',
|
|
694
|
+
title: evidence.path ?? evidence.ref ?? evidence.type,
|
|
695
|
+
status: evidence.confidence,
|
|
696
|
+
jobId: evidence.jobId,
|
|
697
|
+
taskId: evidence.taskId,
|
|
698
|
+
path: evidence.path,
|
|
699
|
+
generatedAt,
|
|
700
|
+
metadata: evidence
|
|
701
|
+
}))
|
|
702
|
+
];
|
|
703
|
+
const edges = [];
|
|
704
|
+
for (const evidence of evidenceRecords) {
|
|
705
|
+
if (evidence.gateId) {
|
|
706
|
+
edges.push({
|
|
707
|
+
kind: 'produces',
|
|
708
|
+
from: graphNodeId('gate', evidence.gateId),
|
|
709
|
+
to: graphNodeId('evidence', evidence.id),
|
|
710
|
+
generatedAt
|
|
711
|
+
});
|
|
712
|
+
edges.push({
|
|
713
|
+
kind: 'verifies',
|
|
714
|
+
from: graphNodeId('evidence', evidence.id),
|
|
715
|
+
to: graphNodeId('gate', evidence.gateId),
|
|
716
|
+
generatedAt
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return createSwarmRunGraph({
|
|
721
|
+
runId: input.runId,
|
|
722
|
+
title: 'Gate and evidence graph',
|
|
723
|
+
generatedAt,
|
|
724
|
+
nodes,
|
|
725
|
+
edges
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
export function createSwarmGateEvidenceGraph(input) {
|
|
729
|
+
return mapSwarmGateEvidenceToGraph(input);
|
|
730
|
+
}
|
|
731
|
+
export function classifySwarmMergeCandidateAdmission(input = {}) {
|
|
732
|
+
const arrayInput = Array.isArray(input);
|
|
733
|
+
const objectInput = arrayInput
|
|
734
|
+
? undefined
|
|
735
|
+
: input;
|
|
736
|
+
const status = objectInput ? normalizeOptionalString(objectInput.status) : undefined;
|
|
737
|
+
if (status)
|
|
738
|
+
return status;
|
|
739
|
+
const reasonCodes = arrayInput
|
|
740
|
+
? normalizeSwarmMergeCandidateReasonCodes(input)
|
|
741
|
+
: normalizeSwarmMergeCandidateReasonCodes(objectInput?.reasonCodes ?? []);
|
|
742
|
+
if (reasonCodes.some((reason) => FRONTIER_SWARM_BLOCKING_MERGE_CANDIDATE_REASON_CODES.has(reason)))
|
|
743
|
+
return 'blocked';
|
|
744
|
+
if (reasonCodes.some((reason) => FRONTIER_SWARM_REVIEW_MERGE_CANDIDATE_REASON_CODES.has(reason)))
|
|
745
|
+
return 'review-required';
|
|
746
|
+
if (reasonCodes.includes('lossy-import'))
|
|
747
|
+
return 'safe-with-losses';
|
|
748
|
+
return 'safe';
|
|
749
|
+
}
|
|
750
|
+
export function createSwarmSemanticChange(input) {
|
|
751
|
+
if (isSwarmSemanticChange(input))
|
|
752
|
+
return cloneJsonValue(input);
|
|
753
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
754
|
+
const symbolId = normalizeId(input.symbolId, 'semantic change symbol id');
|
|
755
|
+
const declarationKind = normalizeId(input.declarationKind, 'semantic change declaration kind');
|
|
756
|
+
const sourceSpan = normalizeSwarmSourceSpan(input.sourceSpan, {
|
|
757
|
+
sourcePath: input.sourcePath,
|
|
758
|
+
sourceHash: input.sourceHash,
|
|
759
|
+
expectedSourceHash: input.expectedSourceHash
|
|
760
|
+
});
|
|
761
|
+
const operation = normalizeOptionalString(input.operation) ?? 'unknown';
|
|
762
|
+
const derivedReasonCodes = deriveSwarmSemanticChangeReasonCodes(input, sourceSpan);
|
|
763
|
+
const reasonCodes = normalizeSwarmMergeCandidateReasonCodes([...(input.reasonCodes ?? []), ...derivedReasonCodes]);
|
|
764
|
+
const conflictReason = normalizeSwarmConflictReason(input.conflictReason, reasonCodes);
|
|
765
|
+
return {
|
|
766
|
+
kind: FRONTIER_SWARM_SEMANTIC_CHANGE_KIND,
|
|
767
|
+
version: FRONTIER_SWARM_SEMANTIC_CHANGE_VERSION,
|
|
768
|
+
id: input.id ?? 'swarm-semantic-change:' + stableHash([symbolId, declarationKind, sourceSpan, operation, reasonCodes]),
|
|
769
|
+
symbolId,
|
|
770
|
+
declarationKind,
|
|
771
|
+
sourceSpan,
|
|
772
|
+
operation,
|
|
773
|
+
confidence: normalizeSwarmSemanticConfidence(input.confidence),
|
|
774
|
+
...(conflictReason ? { conflictReason } : {}),
|
|
775
|
+
status: classifySwarmMergeCandidateAdmission({ status: input.status, reasonCodes }),
|
|
776
|
+
reasonCodes,
|
|
777
|
+
generatedAt,
|
|
778
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
export function createSwarmMergeCandidate(input) {
|
|
782
|
+
if (isSwarmMergeCandidate(input))
|
|
783
|
+
return cloneJsonValue(input);
|
|
784
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
785
|
+
const shorthandChange = input.symbolId || input.declarationKind || input.operation || input.sourceSpan || input.sourcePath
|
|
786
|
+
? [{
|
|
787
|
+
symbolId: input.symbolId ?? 'unknown-symbol',
|
|
788
|
+
declarationKind: input.declarationKind ?? 'unknown',
|
|
789
|
+
operation: input.operation ?? 'unknown',
|
|
790
|
+
sourceSpan: input.sourceSpan,
|
|
791
|
+
sourcePath: input.sourcePath,
|
|
792
|
+
sourceHash: input.sourceHash,
|
|
793
|
+
expectedSourceHash: input.expectedSourceHash,
|
|
794
|
+
confidence: input.confidence,
|
|
795
|
+
conflictReason: input.conflictReason,
|
|
796
|
+
status: input.status,
|
|
797
|
+
reasonCodes: input.reasonCodes,
|
|
798
|
+
generatedAt
|
|
799
|
+
}]
|
|
800
|
+
: [];
|
|
801
|
+
const semanticChanges = [...(input.semanticChanges ?? []), ...(input.changes ?? []), ...shorthandChange]
|
|
802
|
+
.map((change) => createSwarmSemanticChange({ ...change, generatedAt: change.generatedAt ?? generatedAt }));
|
|
803
|
+
const primaryChange = semanticChanges[0];
|
|
804
|
+
const candidateSourceSpan = primaryChange?.sourceSpan ?? normalizeSwarmSourceSpan(input.sourceSpan, {
|
|
805
|
+
sourcePath: input.sourcePath,
|
|
806
|
+
sourceHash: input.sourceHash,
|
|
807
|
+
expectedSourceHash: input.expectedSourceHash
|
|
808
|
+
});
|
|
809
|
+
const derivedReasonCodes = deriveSwarmMergeCandidateReasonCodes(input, semanticChanges, candidateSourceSpan);
|
|
810
|
+
const reasonCodes = normalizeSwarmMergeCandidateReasonCodes([
|
|
811
|
+
...(input.reasonCodes ?? []),
|
|
812
|
+
...semanticChanges.flatMap((change) => change.reasonCodes),
|
|
813
|
+
...derivedReasonCodes
|
|
814
|
+
]);
|
|
815
|
+
const symbolIds = uniqueStrings([
|
|
816
|
+
input.symbolId,
|
|
817
|
+
...semanticChanges.map((change) => change.symbolId)
|
|
818
|
+
]);
|
|
819
|
+
const declarationKinds = uniqueStrings([
|
|
820
|
+
input.declarationKind,
|
|
821
|
+
...semanticChanges.map((change) => change.declarationKind)
|
|
822
|
+
]);
|
|
823
|
+
const changedPaths = uniqueStrings(semanticChanges.map((change) => change.sourceSpan.path).concat(candidateSourceSpan.path));
|
|
824
|
+
const conflictReason = normalizeSwarmConflictReason(input.conflictReason, reasonCodes);
|
|
825
|
+
const metadata = toJsonObject(input.metadata);
|
|
826
|
+
return {
|
|
827
|
+
kind: FRONTIER_SWARM_MERGE_CANDIDATE_KIND,
|
|
828
|
+
version: FRONTIER_SWARM_MERGE_CANDIDATE_VERSION,
|
|
829
|
+
id: input.id ?? 'swarm-merge-candidate:' + stableHash([input.jobId, input.taskId, symbolIds, declarationKinds, semanticChanges, reasonCodes]),
|
|
830
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
831
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
832
|
+
...(input.lane ? { lane: input.lane } : {}),
|
|
833
|
+
...(input.title ? { title: input.title } : {}),
|
|
834
|
+
...(input.sidecarPath ? { sidecarPath: input.sidecarPath } : {}),
|
|
835
|
+
symbolId: symbolIds[0] ?? primaryChange?.symbolId ?? 'unknown-symbol',
|
|
836
|
+
symbolIds,
|
|
837
|
+
declarationKind: declarationKinds[0] ?? primaryChange?.declarationKind ?? 'unknown',
|
|
838
|
+
declarationKinds,
|
|
839
|
+
sourceSpan: candidateSourceSpan,
|
|
840
|
+
operation: input.operation ?? primaryChange?.operation ?? 'unknown',
|
|
841
|
+
confidence: normalizeSwarmSemanticConfidence(input.confidence ?? primaryChange?.confidence),
|
|
842
|
+
...(conflictReason ? { conflictReason } : {}),
|
|
843
|
+
status: classifySwarmMergeCandidateAdmission({ status: input.status, reasonCodes }),
|
|
844
|
+
reasonCodes,
|
|
845
|
+
semanticChanges,
|
|
846
|
+
changedPaths,
|
|
847
|
+
evidencePaths: uniqueStrings(input.evidencePaths ?? []),
|
|
848
|
+
generatedAt,
|
|
849
|
+
summary: {
|
|
850
|
+
changeCount: semanticChanges.length,
|
|
851
|
+
symbolCount: symbolIds.length,
|
|
852
|
+
reasonCount: reasonCodes.length
|
|
853
|
+
},
|
|
854
|
+
...(metadata ? { metadata } : {})
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
export function mapSwarmMergeCandidatesToGraph(input) {
|
|
858
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
859
|
+
const candidates = input.candidates.map((candidate) => createSwarmMergeCandidate({ ...candidate, generatedAt: candidate.generatedAt ?? generatedAt }));
|
|
860
|
+
const includeChangeNodes = input.includeChangeNodes !== false;
|
|
861
|
+
const nodes = [];
|
|
862
|
+
const edges = [];
|
|
863
|
+
for (const candidate of candidates) {
|
|
864
|
+
const candidateNodeId = graphNodeId('candidate', candidate.id);
|
|
865
|
+
nodes.push({
|
|
866
|
+
id: candidateNodeId,
|
|
867
|
+
kind: 'candidate',
|
|
868
|
+
title: candidate.title ?? candidate.symbolId,
|
|
869
|
+
status: candidate.status,
|
|
870
|
+
jobId: candidate.jobId,
|
|
871
|
+
taskId: candidate.taskId,
|
|
872
|
+
path: candidate.changedPaths[0],
|
|
873
|
+
generatedAt,
|
|
874
|
+
metadata: candidate
|
|
875
|
+
});
|
|
876
|
+
if (!includeChangeNodes)
|
|
877
|
+
continue;
|
|
878
|
+
for (const change of candidate.semanticChanges) {
|
|
879
|
+
const changeNodeId = graphNodeId('semantic-change', change.id);
|
|
880
|
+
nodes.push({
|
|
881
|
+
id: changeNodeId,
|
|
882
|
+
kind: 'semantic-change',
|
|
883
|
+
title: `${change.operation} ${change.symbolId}`,
|
|
884
|
+
status: change.status,
|
|
885
|
+
jobId: candidate.jobId,
|
|
886
|
+
taskId: candidate.taskId,
|
|
887
|
+
path: change.sourceSpan.path,
|
|
888
|
+
generatedAt,
|
|
889
|
+
metadata: change
|
|
890
|
+
});
|
|
891
|
+
edges.push({
|
|
892
|
+
kind: 'produces',
|
|
893
|
+
from: candidateNodeId,
|
|
894
|
+
to: changeNodeId,
|
|
895
|
+
label: 'semantic-change',
|
|
896
|
+
generatedAt,
|
|
897
|
+
metadata: {
|
|
898
|
+
symbolId: change.symbolId,
|
|
899
|
+
declarationKind: change.declarationKind,
|
|
900
|
+
operation: change.operation,
|
|
901
|
+
reasonCodes: change.reasonCodes
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
if (change.conflictReason) {
|
|
905
|
+
edges.push({
|
|
906
|
+
kind: 'conflictsWith',
|
|
907
|
+
from: candidateNodeId,
|
|
908
|
+
to: changeNodeId,
|
|
909
|
+
label: change.conflictReason,
|
|
910
|
+
generatedAt
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return { candidates, nodes, edges };
|
|
916
|
+
}
|
|
917
|
+
export function createSwarmMergeCandidateGraph(input) {
|
|
918
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
919
|
+
const projection = mapSwarmMergeCandidatesToGraph({ ...input, generatedAt });
|
|
920
|
+
return createSwarmRunGraph({
|
|
921
|
+
id: input.id,
|
|
922
|
+
runId: input.runId,
|
|
923
|
+
title: input.title ?? 'Semantic merge candidate graph',
|
|
924
|
+
generatedAt,
|
|
925
|
+
nodes: projection.nodes,
|
|
926
|
+
edges: projection.edges,
|
|
927
|
+
metadata: input.metadata
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
export function mapSwarmReplayRecordsToGraph(input) {
|
|
931
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
932
|
+
const replayRecords = (input.replayRecords ?? []).map((record) => isSwarmReplayRecord(record) ? record : createSwarmReplayRecord(record));
|
|
933
|
+
const nodes = [];
|
|
934
|
+
const edges = [];
|
|
935
|
+
for (const replay of replayRecords) {
|
|
936
|
+
const replayNodeId = graphNodeId('replay', replay.id);
|
|
937
|
+
nodes.push({
|
|
938
|
+
id: replayNodeId,
|
|
939
|
+
kind: 'replay',
|
|
940
|
+
title: replay.title,
|
|
941
|
+
status: replay.status,
|
|
942
|
+
jobId: replay.jobId,
|
|
943
|
+
taskId: replay.taskId,
|
|
944
|
+
generatedAt,
|
|
945
|
+
metadata: replay
|
|
946
|
+
});
|
|
947
|
+
for (const event of replay.patchEvents) {
|
|
948
|
+
const eventNodeId = graphNodeId('replay', event.id);
|
|
949
|
+
nodes.push({
|
|
950
|
+
id: eventNodeId,
|
|
951
|
+
kind: 'replay',
|
|
952
|
+
title: `${event.operation} ${event.path}`,
|
|
953
|
+
jobId: event.jobId ?? replay.jobId,
|
|
954
|
+
taskId: event.taskId ?? replay.taskId,
|
|
955
|
+
path: event.path,
|
|
956
|
+
generatedAt,
|
|
957
|
+
metadata: event
|
|
958
|
+
});
|
|
959
|
+
edges.push({
|
|
960
|
+
kind: 'produces',
|
|
961
|
+
from: replayNodeId,
|
|
962
|
+
to: eventNodeId,
|
|
963
|
+
label: event.operation,
|
|
964
|
+
generatedAt
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
for (const evidence of replay.evidenceRefs) {
|
|
968
|
+
const evidenceNodeId = graphNodeId('evidence', evidence.id);
|
|
969
|
+
nodes.push({
|
|
970
|
+
id: evidenceNodeId,
|
|
971
|
+
kind: 'evidence',
|
|
972
|
+
title: evidence.path ?? evidence.uri ?? evidence.id,
|
|
973
|
+
path: evidence.path,
|
|
974
|
+
generatedAt,
|
|
975
|
+
metadata: evidence
|
|
976
|
+
});
|
|
977
|
+
edges.push({
|
|
978
|
+
kind: 'verifies',
|
|
979
|
+
from: evidenceNodeId,
|
|
980
|
+
to: replayNodeId,
|
|
981
|
+
label: evidence.role ?? evidence.kind,
|
|
982
|
+
generatedAt
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return createSwarmRunGraph({
|
|
987
|
+
runId: input.runId,
|
|
988
|
+
title: 'Replay records graph',
|
|
989
|
+
generatedAt,
|
|
990
|
+
nodes,
|
|
991
|
+
edges
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
export function createSwarmReplayRecordGraph(input) {
|
|
995
|
+
return mapSwarmReplayRecordsToGraph(input);
|
|
996
|
+
}
|
|
997
|
+
export function mapSwarmImprovementLoopsToGraph(input) {
|
|
998
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
999
|
+
const loops = (input.improvementLoops ?? []).map((loop) => isSwarmImprovementLoop(loop) ? loop : createSwarmImprovementLoop(loop));
|
|
1000
|
+
const nodes = [];
|
|
1001
|
+
const edges = [];
|
|
1002
|
+
for (const loop of loops) {
|
|
1003
|
+
const phaseNodes = [];
|
|
1004
|
+
if (loop.observation !== undefined) {
|
|
1005
|
+
phaseNodes.push(createImprovementLoopPhaseNode(loop, 'observation', loop.observation, generatedAt));
|
|
1006
|
+
}
|
|
1007
|
+
if (loop.action !== undefined) {
|
|
1008
|
+
phaseNodes.push(createImprovementLoopPhaseNode(loop, 'action', loop.action, generatedAt));
|
|
1009
|
+
}
|
|
1010
|
+
if (loop.result !== undefined) {
|
|
1011
|
+
phaseNodes.push(createImprovementLoopPhaseNode(loop, 'result', loop.result, generatedAt));
|
|
1012
|
+
}
|
|
1013
|
+
if (phaseNodes.length === 0) {
|
|
1014
|
+
phaseNodes.push({
|
|
1015
|
+
id: graphNodeId('rsi', loop.id),
|
|
1016
|
+
kind: 'rsi',
|
|
1017
|
+
title: loop.title,
|
|
1018
|
+
status: loop.status,
|
|
1019
|
+
jobId: loop.jobId,
|
|
1020
|
+
taskId: loop.taskId,
|
|
1021
|
+
generatedAt,
|
|
1022
|
+
metadata: loop
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
nodes.push(...phaseNodes);
|
|
1026
|
+
for (let index = 1; index < phaseNodes.length; index += 1) {
|
|
1027
|
+
edges.push({
|
|
1028
|
+
kind: 'produces',
|
|
1029
|
+
from: phaseNodes[index - 1].id,
|
|
1030
|
+
to: phaseNodes[index].id,
|
|
1031
|
+
label: index === 1 ? 'action' : 'result',
|
|
1032
|
+
generatedAt
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
const entryNodeId = phaseNodes[0]?.id;
|
|
1036
|
+
const exitNodeId = phaseNodes.at(-1)?.id;
|
|
1037
|
+
for (const replayRecordId of loop.replayRecordIds) {
|
|
1038
|
+
const replayNodeId = graphNodeId('replay', replayRecordId);
|
|
1039
|
+
nodes.push({
|
|
1040
|
+
id: replayNodeId,
|
|
1041
|
+
kind: 'replay',
|
|
1042
|
+
title: titleFromId(replayRecordId),
|
|
1043
|
+
generatedAt,
|
|
1044
|
+
metadata: { replayRecordId }
|
|
1045
|
+
});
|
|
1046
|
+
if (entryNodeId)
|
|
1047
|
+
edges.push({ kind: 'produces', from: replayNodeId, to: entryNodeId, label: 'feeds-rsi', generatedAt });
|
|
1048
|
+
}
|
|
1049
|
+
for (const patchEventId of loop.patchEventIds) {
|
|
1050
|
+
const patchNodeId = graphNodeId('replay', patchEventId);
|
|
1051
|
+
nodes.push({
|
|
1052
|
+
id: patchNodeId,
|
|
1053
|
+
kind: 'replay',
|
|
1054
|
+
title: titleFromId(patchEventId),
|
|
1055
|
+
generatedAt,
|
|
1056
|
+
metadata: { patchEventId }
|
|
1057
|
+
});
|
|
1058
|
+
if (entryNodeId)
|
|
1059
|
+
edges.push({ kind: 'produces', from: patchNodeId, to: entryNodeId, label: 'feeds-rsi', generatedAt });
|
|
1060
|
+
}
|
|
1061
|
+
for (const evidence of loop.evidenceRefs) {
|
|
1062
|
+
const evidenceNodeId = graphNodeId('evidence', evidence.id);
|
|
1063
|
+
nodes.push({
|
|
1064
|
+
id: evidenceNodeId,
|
|
1065
|
+
kind: 'evidence',
|
|
1066
|
+
title: evidence.path ?? evidence.uri ?? evidence.id,
|
|
1067
|
+
path: evidence.path,
|
|
1068
|
+
generatedAt,
|
|
1069
|
+
metadata: evidence
|
|
1070
|
+
});
|
|
1071
|
+
if (exitNodeId)
|
|
1072
|
+
edges.push({ kind: 'verifies', from: evidenceNodeId, to: exitNodeId, label: evidence.role ?? evidence.kind, generatedAt });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return createSwarmRunGraph({
|
|
1076
|
+
runId: input.runId,
|
|
1077
|
+
title: 'Improvement loop graph',
|
|
1078
|
+
generatedAt,
|
|
1079
|
+
nodes,
|
|
1080
|
+
edges
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
export function createSwarmImprovementLoopGraph(input) {
|
|
1084
|
+
return mapSwarmImprovementLoopsToGraph(input);
|
|
1085
|
+
}
|
|
1086
|
+
export function createSwarmRunGraphChunk(input = {}) {
|
|
1087
|
+
const generatedAt = Date.now();
|
|
1088
|
+
const nodes = dedupeGraphNodes((input.nodes ?? []).map((node) => normalizeGraphNode(node, generatedAt)));
|
|
1089
|
+
const known = new Set(nodes.map((node) => node.id));
|
|
1090
|
+
const edges = dedupeGraphEdges((input.edges ?? [])
|
|
1091
|
+
.map((edge) => normalizeGraphEdge(edge, generatedAt))
|
|
1092
|
+
.filter((edge) => known.has(edge.from) && known.has(edge.to)));
|
|
1093
|
+
const entryNodeIds = uniqueStrings((input.entryNodeIds ?? []).filter((id) => known.has(id)));
|
|
1094
|
+
const exitNodeIds = uniqueStrings((input.exitNodeIds ?? []).filter((id) => known.has(id)));
|
|
1095
|
+
return {
|
|
1096
|
+
kind: FRONTIER_SWARM_RUN_GRAPH_CHUNK_KIND,
|
|
1097
|
+
version: FRONTIER_SWARM_RUN_GRAPH_CHUNK_VERSION,
|
|
1098
|
+
id: input.id ?? 'swarm-run-graph-chunk:' + stableHash([nodes.map((node) => node.id), edges.map((edge) => edge.id), entryNodeIds, exitNodeIds]),
|
|
1099
|
+
nodes,
|
|
1100
|
+
edges,
|
|
1101
|
+
entryNodeIds,
|
|
1102
|
+
exitNodeIds,
|
|
1103
|
+
summary: {
|
|
1104
|
+
nodeCount: nodes.length,
|
|
1105
|
+
edgeCount: edges.length,
|
|
1106
|
+
entryCount: entryNodeIds.length,
|
|
1107
|
+
exitCount: exitNodeIds.length
|
|
1108
|
+
},
|
|
1109
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
export function createSwarmRunGraphChain(input) {
|
|
1113
|
+
const nodes = input.nodes.map((node) => normalizeGraphNode(node));
|
|
1114
|
+
const edges = [];
|
|
1115
|
+
for (let index = 1; index < nodes.length; index += 1) {
|
|
1116
|
+
edges.push({ kind: input.edgeKind ?? 'dependsOn', from: nodes[index - 1].id, to: nodes[index].id });
|
|
1117
|
+
}
|
|
1118
|
+
return createSwarmRunGraphChunk({
|
|
1119
|
+
id: input.id,
|
|
1120
|
+
nodes,
|
|
1121
|
+
edges,
|
|
1122
|
+
entryNodeIds: nodes[0] ? [nodes[0].id] : [],
|
|
1123
|
+
exitNodeIds: nodes.at(-1) ? [nodes.at(-1).id] : [],
|
|
1124
|
+
metadata: input.metadata
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
export function createSwarmRunGraphFork(input) {
|
|
1128
|
+
const source = normalizeGraphNode(input.source);
|
|
1129
|
+
const branches = input.branches.map((node) => normalizeGraphNode(node));
|
|
1130
|
+
return createSwarmRunGraphChunk({
|
|
1131
|
+
id: input.id,
|
|
1132
|
+
nodes: [source, ...branches],
|
|
1133
|
+
edges: branches.map((branch) => ({ kind: 'dependsOn', from: source.id, to: branch.id, label: 'fork' })),
|
|
1134
|
+
entryNodeIds: [source.id],
|
|
1135
|
+
exitNodeIds: branches.map((branch) => branch.id),
|
|
1136
|
+
metadata: input.metadata
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
export function createSwarmRunGraphJoin(input) {
|
|
1140
|
+
const branches = input.branches.map((node) => normalizeGraphNode(node));
|
|
1141
|
+
const join = normalizeGraphNode(input.join);
|
|
1142
|
+
return createSwarmRunGraphChunk({
|
|
1143
|
+
id: input.id,
|
|
1144
|
+
nodes: [...branches, join],
|
|
1145
|
+
edges: branches.map((branch) => ({ kind: 'dependsOn', from: branch.id, to: join.id, label: 'join' })),
|
|
1146
|
+
entryNodeIds: branches.map((branch) => branch.id),
|
|
1147
|
+
exitNodeIds: [join.id],
|
|
1148
|
+
metadata: input.metadata
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
export function createSwarmRunGraphBarrier(input) {
|
|
1152
|
+
const prerequisites = input.prerequisites.map((node) => normalizeGraphNode(node));
|
|
1153
|
+
const prerequisiteIds = prerequisites.map((node) => node.id);
|
|
1154
|
+
const barrierMetadata = toJsonObject(input.barrier?.metadata);
|
|
1155
|
+
const barrier = normalizeGraphNode({
|
|
1156
|
+
...(input.barrier ?? {}),
|
|
1157
|
+
id: input.barrier?.id ?? (input.id ? `${input.id}:barrier` : graphNodeId('gate', 'barrier:' + stableHash(prerequisiteIds))),
|
|
1158
|
+
kind: input.barrier?.kind ?? 'gate',
|
|
1159
|
+
title: input.barrier?.title ?? 'Barrier',
|
|
1160
|
+
metadata: {
|
|
1161
|
+
...(barrierMetadata ?? {}),
|
|
1162
|
+
role: 'barrier',
|
|
1163
|
+
graphChunkKind: 'barrier',
|
|
1164
|
+
prerequisiteIds,
|
|
1165
|
+
prerequisiteCount: prerequisiteIds.length
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
return createSwarmRunGraphChunk({
|
|
1169
|
+
id: input.id,
|
|
1170
|
+
nodes: [...prerequisites, barrier],
|
|
1171
|
+
edges: prerequisites.map((node, index) => ({
|
|
1172
|
+
kind: 'dependsOn',
|
|
1173
|
+
from: node.id,
|
|
1174
|
+
to: barrier.id,
|
|
1175
|
+
label: 'barrier',
|
|
1176
|
+
metadata: {
|
|
1177
|
+
role: 'barrier-prerequisite',
|
|
1178
|
+
barrierId: barrier.id,
|
|
1179
|
+
prerequisiteId: node.id,
|
|
1180
|
+
prerequisiteIndex: index,
|
|
1181
|
+
prerequisiteCount: prerequisites.length
|
|
1182
|
+
}
|
|
1183
|
+
})),
|
|
1184
|
+
entryNodeIds: prerequisiteIds,
|
|
1185
|
+
exitNodeIds: [barrier.id],
|
|
1186
|
+
metadata: input.metadata
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
export function createSwarmRunGraphRaceSelect(input) {
|
|
1190
|
+
const baseBranches = input.branches.map((node) => normalizeGraphNode(node));
|
|
1191
|
+
const branchIds = baseBranches.map((node) => node.id);
|
|
1192
|
+
const selectedBranchId = input.selectedBranchId && branchIds.includes(input.selectedBranchId) ? input.selectedBranchId : undefined;
|
|
1193
|
+
const rejectedBranchIds = new Set(selectedBranchId
|
|
1194
|
+
? branchIds.filter((id) => id !== selectedBranchId)
|
|
1195
|
+
: uniqueStrings(input.rejectedBranchIds ?? []).filter((id) => branchIds.includes(id)));
|
|
1196
|
+
if (selectedBranchId)
|
|
1197
|
+
rejectedBranchIds.delete(selectedBranchId);
|
|
1198
|
+
const selectorMetadata = toJsonObject(input.selector?.metadata);
|
|
1199
|
+
const selector = normalizeGraphNode({
|
|
1200
|
+
...(input.selector ?? {}),
|
|
1201
|
+
id: input.selector?.id ?? (input.id ? `${input.id}:selector` : graphNodeId('decision', 'race-select:' + stableHash(branchIds))),
|
|
1202
|
+
kind: input.selector?.kind ?? 'decision',
|
|
1203
|
+
title: input.selector?.title ?? 'Race/select',
|
|
1204
|
+
metadata: {
|
|
1205
|
+
...(selectorMetadata ?? {}),
|
|
1206
|
+
role: 'race-select',
|
|
1207
|
+
graphChunkKind: 'race-select',
|
|
1208
|
+
branchIds,
|
|
1209
|
+
branchCount: branchIds.length,
|
|
1210
|
+
...(selectedBranchId ? { selectedBranchId } : {}),
|
|
1211
|
+
rejectedBranchIds: Array.from(rejectedBranchIds).sort()
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
const branches = baseBranches.map((node, index) => {
|
|
1215
|
+
const disposition = node.id === selectedBranchId ? 'selected' : rejectedBranchIds.has(node.id) ? 'rejected' : 'candidate';
|
|
1216
|
+
return normalizeGraphNode({
|
|
1217
|
+
...node,
|
|
1218
|
+
status: node.status ?? (disposition === 'selected' ? 'accepted' : disposition === 'rejected' ? 'rejected' : undefined),
|
|
1219
|
+
metadata: {
|
|
1220
|
+
...(node.metadata ?? {}),
|
|
1221
|
+
raceSelect: {
|
|
1222
|
+
selectorId: selector.id,
|
|
1223
|
+
branchId: node.id,
|
|
1224
|
+
branchIndex: index,
|
|
1225
|
+
branchCount: baseBranches.length,
|
|
1226
|
+
disposition,
|
|
1227
|
+
selected: disposition === 'selected',
|
|
1228
|
+
rejected: disposition === 'rejected'
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}, node.generatedAt);
|
|
1232
|
+
});
|
|
1233
|
+
return createSwarmRunGraphChunk({
|
|
1234
|
+
id: input.id,
|
|
1235
|
+
nodes: [selector, ...branches],
|
|
1236
|
+
edges: branches.map((branch, index) => {
|
|
1237
|
+
const disposition = branch.id === selectedBranchId ? 'selected' : rejectedBranchIds.has(branch.id) ? 'rejected' : 'candidate';
|
|
1238
|
+
return {
|
|
1239
|
+
kind: disposition === 'selected' ? 'mergesInto' : disposition === 'rejected' ? 'supersedes' : 'dependsOn',
|
|
1240
|
+
from: branch.id,
|
|
1241
|
+
to: selector.id,
|
|
1242
|
+
label: disposition,
|
|
1243
|
+
metadata: {
|
|
1244
|
+
role: 'race-select-branch',
|
|
1245
|
+
selectorId: selector.id,
|
|
1246
|
+
branchId: branch.id,
|
|
1247
|
+
branchIndex: index,
|
|
1248
|
+
branchCount: branches.length,
|
|
1249
|
+
disposition,
|
|
1250
|
+
selected: disposition === 'selected',
|
|
1251
|
+
rejected: disposition === 'rejected'
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
}),
|
|
1255
|
+
entryNodeIds: branchIds,
|
|
1256
|
+
exitNodeIds: [selector.id],
|
|
1257
|
+
metadata: input.metadata
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
export function createSwarmRunGraphTournament(input) {
|
|
1261
|
+
const tournament = normalizeGraphNode({ id: input.id ? `${input.id}:tournament` : undefined, kind: 'decision', title: 'Tournament', metadata: input.metadata });
|
|
1262
|
+
const candidates = input.candidates.map((node) => normalizeGraphNode({ ...node, kind: node.kind ?? 'candidate' }));
|
|
1263
|
+
const rejected = new Set(input.rejectedCandidates ?? []);
|
|
1264
|
+
const edges = candidates.map((candidate) => ({
|
|
1265
|
+
kind: candidate.id === input.winner ? 'mergesInto' : rejected.has(candidate.id) ? 'supersedes' : 'dependsOn',
|
|
1266
|
+
from: candidate.id,
|
|
1267
|
+
to: tournament.id,
|
|
1268
|
+
label: candidate.id === input.winner ? 'winner' : rejected.has(candidate.id) ? 'rejected' : 'candidate'
|
|
1269
|
+
}));
|
|
1270
|
+
return createSwarmRunGraphChunk({
|
|
1271
|
+
id: input.id,
|
|
1272
|
+
nodes: [tournament, ...candidates],
|
|
1273
|
+
edges,
|
|
1274
|
+
entryNodeIds: candidates.map((candidate) => candidate.id),
|
|
1275
|
+
exitNodeIds: [tournament.id],
|
|
1276
|
+
metadata: input.metadata
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
export function createSwarmRunGraphRetryLoop(input) {
|
|
1280
|
+
const action = normalizeGraphNode(input.action);
|
|
1281
|
+
const gate = normalizeGraphNode({ ...input.gate, kind: input.gate.kind ?? 'gate' });
|
|
1282
|
+
const retry = normalizeGraphNode(input.retry);
|
|
1283
|
+
const optional = [input.success, input.failure].filter((node) => Boolean(node)).map((node) => normalizeGraphNode(node));
|
|
1284
|
+
return createSwarmRunGraphChunk({
|
|
1285
|
+
id: input.id,
|
|
1286
|
+
nodes: [action, gate, retry, ...optional],
|
|
1287
|
+
edges: [
|
|
1288
|
+
{ kind: 'verifies', from: action.id, to: gate.id },
|
|
1289
|
+
{ kind: 'blocks', from: gate.id, to: retry.id, label: 'retry' },
|
|
1290
|
+
{ kind: 'dependsOn', from: retry.id, to: action.id, label: 'loop' },
|
|
1291
|
+
...optional.map((node) => ({ kind: node.status === 'failed' ? 'blocks' : 'mergesInto', from: gate.id, to: node.id }))
|
|
1292
|
+
],
|
|
1293
|
+
entryNodeIds: [action.id],
|
|
1294
|
+
exitNodeIds: optional.length > 0 ? optional.map((node) => node.id) : [gate.id],
|
|
1295
|
+
metadata: input.metadata
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
export function createSwarmRunGraphRsiLoop(input) {
|
|
1299
|
+
const observe = normalizeGraphNode({ ...input.observe, kind: input.observe.kind ?? 'rsi' });
|
|
1300
|
+
const improve = normalizeGraphNode({ ...input.improve, kind: input.improve.kind ?? 'rsi' });
|
|
1301
|
+
const apply = input.apply ? normalizeGraphNode(input.apply) : undefined;
|
|
1302
|
+
return createSwarmRunGraphChunk({
|
|
1303
|
+
id: input.id,
|
|
1304
|
+
nodes: apply ? [observe, improve, apply] : [observe, improve],
|
|
1305
|
+
edges: [
|
|
1306
|
+
{ kind: 'produces', from: observe.id, to: improve.id, label: 'feedback' },
|
|
1307
|
+
...(apply ? [{ kind: 'mergesInto', from: improve.id, to: apply.id, label: 'applies' }] : [])
|
|
1308
|
+
],
|
|
1309
|
+
entryNodeIds: [observe.id],
|
|
1310
|
+
exitNodeIds: [apply?.id ?? improve.id],
|
|
1311
|
+
metadata: input.metadata
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
export function createSwarmRunGraphSynthesisChunk(input = {}) {
|
|
1315
|
+
const source = input.source ? normalizeGraphNode(input.source) : undefined;
|
|
1316
|
+
const panel = normalizeGraphNode({
|
|
1317
|
+
...input.panel,
|
|
1318
|
+
id: input.panel?.id ?? (input.id ? `${input.id}:panel` : undefined),
|
|
1319
|
+
kind: input.panel?.kind ?? 'decision',
|
|
1320
|
+
title: input.panel?.title ?? 'Synthesis panel'
|
|
1321
|
+
});
|
|
1322
|
+
const candidates = (input.candidates ?? []).map((candidate) => normalizeGraphNode({
|
|
1323
|
+
...candidate,
|
|
1324
|
+
kind: candidate.kind ?? 'candidate'
|
|
1325
|
+
}));
|
|
1326
|
+
const rejectedCandidateIds = new Set(input.rejectedCandidateIds ?? candidates
|
|
1327
|
+
.filter((candidate) => candidate.status === 'rejected' || candidate.status === 'failed' || candidate.status === 'superseded')
|
|
1328
|
+
.map((candidate) => candidate.id));
|
|
1329
|
+
const selectedCandidateId = input.selectedCandidateId ?? candidates.find((candidate) => !rejectedCandidateIds.has(candidate.id))?.id;
|
|
1330
|
+
const selectedCandidate = selectedCandidateId ? candidates.find((candidate) => candidate.id === selectedCandidateId) : undefined;
|
|
1331
|
+
const decision = input.decision ? normalizeGraphNode({
|
|
1332
|
+
...input.decision,
|
|
1333
|
+
id: input.decision.id ?? (input.id ? `${input.id}:decision` : undefined),
|
|
1334
|
+
kind: input.decision.kind ?? 'decision',
|
|
1335
|
+
title: input.decision.title ?? 'Coordinator synthesis decision'
|
|
1336
|
+
}) : undefined;
|
|
1337
|
+
const output = input.output ? normalizeGraphNode({
|
|
1338
|
+
...input.output,
|
|
1339
|
+
kind: input.output.kind ?? 'candidate'
|
|
1340
|
+
}) : undefined;
|
|
1341
|
+
const edges = [];
|
|
1342
|
+
if (source) {
|
|
1343
|
+
edges.push({ kind: 'produces', from: source.id, to: panel.id, label: 'synthesis-request' });
|
|
1344
|
+
}
|
|
1345
|
+
for (const candidate of candidates) {
|
|
1346
|
+
edges.push({ kind: 'produces', from: panel.id, to: candidate.id, label: 'candidate' });
|
|
1347
|
+
}
|
|
1348
|
+
if (decision && selectedCandidate) {
|
|
1349
|
+
edges.push({ kind: 'mergesInto', from: selectedCandidate.id, to: decision.id, label: 'selected' });
|
|
1350
|
+
}
|
|
1351
|
+
for (const candidate of candidates) {
|
|
1352
|
+
if (!rejectedCandidateIds.has(candidate.id))
|
|
1353
|
+
continue;
|
|
1354
|
+
edges.push(selectedCandidate && selectedCandidate.id !== candidate.id
|
|
1355
|
+
? { kind: 'supersedes', from: selectedCandidate.id, to: candidate.id, label: 'rejected' }
|
|
1356
|
+
: { kind: 'supersedes', from: candidate.id, to: decision?.id ?? panel.id, label: 'rejected' });
|
|
1357
|
+
}
|
|
1358
|
+
if (decision && output) {
|
|
1359
|
+
edges.push({
|
|
1360
|
+
kind: isBlockingGraphStatus(decision.status) ? 'blocks' : 'produces',
|
|
1361
|
+
from: decision.id,
|
|
1362
|
+
to: output.id,
|
|
1363
|
+
label: isBlockingGraphStatus(decision.status) ? 'blocked-output' : 'synthesized-output'
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
else if (selectedCandidate && output) {
|
|
1367
|
+
edges.push({ kind: 'mergesInto', from: selectedCandidate.id, to: output.id, label: 'synthesized-output' });
|
|
1368
|
+
}
|
|
1369
|
+
const nodes = [source, panel, ...candidates, decision, output].filter((node) => Boolean(node));
|
|
1370
|
+
return createSwarmRunGraphChunk({
|
|
1371
|
+
id: input.id,
|
|
1372
|
+
nodes,
|
|
1373
|
+
edges,
|
|
1374
|
+
entryNodeIds: [source?.id ?? panel.id],
|
|
1375
|
+
exitNodeIds: [output?.id ?? decision?.id ?? selectedCandidate?.id ?? panel.id],
|
|
1376
|
+
metadata: input.metadata
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
export function createSwarmRunGraphVerificationGateChunk(input) {
|
|
1380
|
+
const subject = normalizeGraphNode(input.subject);
|
|
1381
|
+
const gate = normalizeGraphNode({
|
|
1382
|
+
...input.gate,
|
|
1383
|
+
kind: input.gate.kind ?? 'gate',
|
|
1384
|
+
title: input.gate.title ?? 'Verification gate'
|
|
1385
|
+
});
|
|
1386
|
+
const evidence = (input.evidence ?? []).map((node) => normalizeGraphNode({
|
|
1387
|
+
...node,
|
|
1388
|
+
kind: node.kind ?? 'evidence'
|
|
1389
|
+
}));
|
|
1390
|
+
const pass = input.pass ? normalizeGraphNode(input.pass) : undefined;
|
|
1391
|
+
const block = input.block ? normalizeGraphNode(input.block) : undefined;
|
|
1392
|
+
const passing = isPassingGraphStatus(gate.status);
|
|
1393
|
+
const blocking = isBlockingGraphStatus(gate.status);
|
|
1394
|
+
const edges = [
|
|
1395
|
+
{ kind: 'verifies', from: gate.id, to: subject.id, label: 'gate' },
|
|
1396
|
+
...evidence.map((node) => ({ kind: 'produces', from: gate.id, to: node.id, label: 'evidence' })),
|
|
1397
|
+
...evidence.map((node) => ({ kind: 'verifies', from: node.id, to: gate.id, label: 'evidence-verifies-gate' }))
|
|
1398
|
+
];
|
|
1399
|
+
if (passing && pass) {
|
|
1400
|
+
edges.push({ kind: 'mergesInto', from: gate.id, to: pass.id, label: 'passed' });
|
|
1401
|
+
}
|
|
1402
|
+
if (blocking) {
|
|
1403
|
+
edges.push({ kind: 'blocks', from: gate.id, to: block?.id ?? subject.id, label: gate.status === 'failed' ? 'failed' : 'blocked' });
|
|
1404
|
+
}
|
|
1405
|
+
return createSwarmRunGraphChunk({
|
|
1406
|
+
id: input.id,
|
|
1407
|
+
nodes: [subject, gate, ...evidence, pass, block].filter((node) => Boolean(node)),
|
|
1408
|
+
edges,
|
|
1409
|
+
entryNodeIds: [subject.id],
|
|
1410
|
+
exitNodeIds: [passing && pass ? pass.id : blocking && block ? block.id : gate.id],
|
|
1411
|
+
metadata: input.metadata
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
export function createSwarmRunGraphMergeGateChunk(input) {
|
|
1415
|
+
const candidate = normalizeGraphNode({
|
|
1416
|
+
...input.candidate,
|
|
1417
|
+
kind: input.candidate.kind ?? 'candidate'
|
|
1418
|
+
});
|
|
1419
|
+
const target = normalizeGraphNode(input.target);
|
|
1420
|
+
const gate = input.gate ? normalizeGraphNode({
|
|
1421
|
+
...input.gate,
|
|
1422
|
+
kind: input.gate.kind ?? 'gate',
|
|
1423
|
+
title: input.gate.title ?? 'Merge gate'
|
|
1424
|
+
}) : undefined;
|
|
1425
|
+
const decision = input.decision ? normalizeGraphNode({
|
|
1426
|
+
...input.decision,
|
|
1427
|
+
kind: input.decision.kind ?? 'decision',
|
|
1428
|
+
title: input.decision.title ?? 'Coordinator merge decision'
|
|
1429
|
+
}) : undefined;
|
|
1430
|
+
const blockers = (input.blockers ?? []).map((node) => normalizeGraphNode({
|
|
1431
|
+
...node,
|
|
1432
|
+
kind: node.kind ?? 'gate'
|
|
1433
|
+
}));
|
|
1434
|
+
const superseded = (input.superseded ?? []).map((node) => normalizeGraphNode({
|
|
1435
|
+
...node,
|
|
1436
|
+
kind: node.kind ?? 'candidate'
|
|
1437
|
+
}));
|
|
1438
|
+
const status = input.status ?? decision?.status ?? gate?.status ?? candidate.status;
|
|
1439
|
+
const passing = isPassingGraphStatus(status);
|
|
1440
|
+
const blocking = isBlockingGraphStatus(status);
|
|
1441
|
+
const edges = [];
|
|
1442
|
+
if (gate) {
|
|
1443
|
+
edges.push({ kind: 'verifies', from: gate.id, to: candidate.id, label: 'merge-gate' });
|
|
1444
|
+
}
|
|
1445
|
+
if (gate && decision) {
|
|
1446
|
+
edges.push({ kind: blocking ? 'blocks' : 'produces', from: gate.id, to: decision.id, label: 'gate-decision' });
|
|
1447
|
+
}
|
|
1448
|
+
if (decision) {
|
|
1449
|
+
edges.push({ kind: 'verifies', from: decision.id, to: candidate.id, label: 'coordinator-decision' });
|
|
1450
|
+
}
|
|
1451
|
+
for (const blocker of blockers) {
|
|
1452
|
+
edges.push({ kind: 'blocks', from: blocker.id, to: candidate.id, label: 'merge-blocker' });
|
|
1453
|
+
}
|
|
1454
|
+
for (const node of superseded) {
|
|
1455
|
+
edges.push({ kind: 'supersedes', from: candidate.id, to: node.id, label: 'superseded' });
|
|
1456
|
+
}
|
|
1457
|
+
if (passing) {
|
|
1458
|
+
edges.push({ kind: 'mergesInto', from: candidate.id, to: target.id, label: 'accepted' });
|
|
1459
|
+
}
|
|
1460
|
+
else if (blocking) {
|
|
1461
|
+
edges.push({ kind: 'blocks', from: decision?.id ?? gate?.id ?? candidate.id, to: target.id, label: status === 'failed' ? 'failed' : 'blocked' });
|
|
1462
|
+
}
|
|
1463
|
+
return createSwarmRunGraphChunk({
|
|
1464
|
+
id: input.id,
|
|
1465
|
+
nodes: [candidate, target, gate, decision, ...blockers, ...superseded].filter((node) => Boolean(node)),
|
|
1466
|
+
edges,
|
|
1467
|
+
entryNodeIds: [candidate.id],
|
|
1468
|
+
exitNodeIds: [passing ? target.id : blocking ? decision?.id ?? gate?.id ?? candidate.id : target.id],
|
|
1469
|
+
metadata: input.metadata
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
381
1472
|
export const FRONTIER_SWARM_TASK_MODEL_PROFILES = [
|
|
382
1473
|
{
|
|
383
1474
|
workKind: 'agent-task',
|
|
@@ -748,7 +1839,498 @@ export function createSwarmRun(input) {
|
|
|
748
1839
|
summary: summarizeRun(input.plan.jobs, results),
|
|
749
1840
|
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
750
1841
|
};
|
|
751
|
-
return run;
|
|
1842
|
+
return run;
|
|
1843
|
+
}
|
|
1844
|
+
export function createRunEventsFromSwarmPlan(plan, options = {}) {
|
|
1845
|
+
const actorId = options.actorId ?? 'frontier-swarm';
|
|
1846
|
+
const runId = options.runId ?? plan.runId;
|
|
1847
|
+
const time = swarmRunEventTime(options, plan.createdAt);
|
|
1848
|
+
let actorSeq = options.startActorSeq ?? 1;
|
|
1849
|
+
const events = [];
|
|
1850
|
+
const created = createRunEvent({
|
|
1851
|
+
runId,
|
|
1852
|
+
actorId,
|
|
1853
|
+
actorSeq: actorSeq++,
|
|
1854
|
+
parents: [...(options.parents ?? [])],
|
|
1855
|
+
time,
|
|
1856
|
+
type: 'run.created',
|
|
1857
|
+
payload: {
|
|
1858
|
+
goal: plan.metadata?.objective ?? plan.metadata?.goal ?? `Frontier swarm ${plan.id}`,
|
|
1859
|
+
metadata: pruneUndefinedJsonObject({
|
|
1860
|
+
source: 'frontier-swarm.plan',
|
|
1861
|
+
planId: plan.id,
|
|
1862
|
+
manifestId: plan.manifestId,
|
|
1863
|
+
summary: plan.summary,
|
|
1864
|
+
filters: plan.filters,
|
|
1865
|
+
limits: plan.limits,
|
|
1866
|
+
metadata: plan.metadata,
|
|
1867
|
+
adapterMetadata: toJsonObject(options.metadata)
|
|
1868
|
+
})
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
events.push(created);
|
|
1872
|
+
const laneIds = uniqueStrings(plan.jobs.map((job) => job.lane));
|
|
1873
|
+
const laneEventByLane = new Map();
|
|
1874
|
+
for (const lane of laneIds) {
|
|
1875
|
+
const laneJobs = plan.jobs.filter((job) => job.lane === lane);
|
|
1876
|
+
const laneEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunLane({
|
|
1877
|
+
id: swarmRunLaneNodeId(lane),
|
|
1878
|
+
title: lane,
|
|
1879
|
+
packageId: inferSwarmLanePackageId(laneJobs),
|
|
1880
|
+
allowedWrites: uniqueStrings(laneJobs.flatMap((job) => job.allowedWrites)),
|
|
1881
|
+
requiredChecks: uniqueStrings(laneJobs.flatMap((job) => job.verification.map(formatSwarmCommandLine))),
|
|
1882
|
+
maxConcurrency: plan.limits.maxLaneConcurrency[lane],
|
|
1883
|
+
concurrencyKey: lane,
|
|
1884
|
+
semanticRegions: uniqueStrings(laneJobs.flatMap((job) => [...job.ownedRegions, ...job.changedRegions])),
|
|
1885
|
+
status: 'ready',
|
|
1886
|
+
createdAt: time,
|
|
1887
|
+
updatedAt: time,
|
|
1888
|
+
metadata: pruneUndefinedJsonObject({
|
|
1889
|
+
source: 'frontier-swarm.plan.lane',
|
|
1890
|
+
jobIds: laneJobs.map((job) => job.id),
|
|
1891
|
+
taskIds: uniqueStrings(laneJobs.map((job) => job.taskId))
|
|
1892
|
+
})
|
|
1893
|
+
}), {
|
|
1894
|
+
parents: [created.id],
|
|
1895
|
+
time
|
|
1896
|
+
});
|
|
1897
|
+
events.push(laneEvent);
|
|
1898
|
+
laneEventByLane.set(lane, laneEvent);
|
|
1899
|
+
}
|
|
1900
|
+
const taskById = new Map();
|
|
1901
|
+
for (const job of plan.jobs)
|
|
1902
|
+
if (!taskById.has(job.taskId))
|
|
1903
|
+
taskById.set(job.taskId, job);
|
|
1904
|
+
for (const job of taskById.values()) {
|
|
1905
|
+
const task = job.task;
|
|
1906
|
+
const taskEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunTask({
|
|
1907
|
+
id: swarmRunTaskNodeId(job.taskId),
|
|
1908
|
+
title: task.title || job.title,
|
|
1909
|
+
laneId: swarmRunLaneNodeId(job.lane),
|
|
1910
|
+
status: swarmStatusToRunStatus(job.status),
|
|
1911
|
+
targetRefs: uniqueStrings([...(task.targetRefs ?? []), ...job.allowedWrites]),
|
|
1912
|
+
sourceRefs: task.sourceRefs,
|
|
1913
|
+
allowedWrites: job.allowedWrites,
|
|
1914
|
+
semanticRegions: uniqueStrings([...job.ownedRegions, ...job.changedRegions]),
|
|
1915
|
+
acceptance: job.acceptance,
|
|
1916
|
+
verification: job.verification.map((command) => ({
|
|
1917
|
+
id: command.name,
|
|
1918
|
+
command: command.command,
|
|
1919
|
+
args: command.args,
|
|
1920
|
+
cwd: command.cwd,
|
|
1921
|
+
required: command.required,
|
|
1922
|
+
metadata: command.metadata
|
|
1923
|
+
})),
|
|
1924
|
+
priority: job.priority,
|
|
1925
|
+
concurrencyKey: job.concurrencyKey,
|
|
1926
|
+
createdAt: time,
|
|
1927
|
+
updatedAt: time,
|
|
1928
|
+
metadata: pruneUndefinedJsonObject({
|
|
1929
|
+
source: 'frontier-swarm.plan.task',
|
|
1930
|
+
jobIds: plan.jobs.filter((entry) => entry.taskId === job.taskId).map((entry) => entry.id),
|
|
1931
|
+
workKind: task.workKind,
|
|
1932
|
+
layer: job.layer,
|
|
1933
|
+
compute: job.compute.id,
|
|
1934
|
+
tags: job.tags,
|
|
1935
|
+
task: task
|
|
1936
|
+
})
|
|
1937
|
+
}), {
|
|
1938
|
+
parents: [laneEventByLane.get(job.lane)?.id ?? created.id],
|
|
1939
|
+
time
|
|
1940
|
+
});
|
|
1941
|
+
events.push(taskEvent);
|
|
1942
|
+
}
|
|
1943
|
+
return events;
|
|
1944
|
+
}
|
|
1945
|
+
export function createRunEventsFromSwarmLease(lease, options = {}) {
|
|
1946
|
+
const actorId = options.actorId ?? lease.workerId ?? 'frontier-swarm';
|
|
1947
|
+
const runId = options.runId ?? options.job?.metadata?.runId ?? 'frontier-swarm';
|
|
1948
|
+
const eventType = options.eventType ?? (lease.status === 'released' ? 'lease.released' : 'lease.granted');
|
|
1949
|
+
const time = swarmRunEventTime(options, eventType === 'lease.released' ? options.now : lease.leasedAt);
|
|
1950
|
+
const leaseNode = {
|
|
1951
|
+
kind: 'lease',
|
|
1952
|
+
id: swarmRunLeaseNodeId(lease.id),
|
|
1953
|
+
title: `Lease ${lease.jobId}`,
|
|
1954
|
+
scopeId: options.job ? swarmRunTaskNodeId(options.job.taskId) : swarmRunTaskNodeId(lease.jobId),
|
|
1955
|
+
leaseKey: options.job?.concurrencyKey ?? lease.jobId,
|
|
1956
|
+
holderId: lease.workerId,
|
|
1957
|
+
status: eventType === 'lease.released' ? 'released' : 'granted',
|
|
1958
|
+
requestedAt: new Date(lease.leasedAt).toISOString(),
|
|
1959
|
+
grantedAt: new Date(lease.leasedAt).toISOString(),
|
|
1960
|
+
releasedAt: eventType === 'lease.released' ? time : undefined,
|
|
1961
|
+
metadata: pruneUndefinedJsonObject({
|
|
1962
|
+
source: 'frontier-swarm.lease',
|
|
1963
|
+
jobId: lease.jobId,
|
|
1964
|
+
token: lease.token,
|
|
1965
|
+
fencingToken: lease.fencingToken,
|
|
1966
|
+
expiresAt: lease.expiresAt,
|
|
1967
|
+
status: lease.status
|
|
1968
|
+
})
|
|
1969
|
+
};
|
|
1970
|
+
return [createRunEvent({
|
|
1971
|
+
runId,
|
|
1972
|
+
actorId,
|
|
1973
|
+
actorSeq: options.startActorSeq ?? 1,
|
|
1974
|
+
parents: [...(options.parents ?? [])],
|
|
1975
|
+
time,
|
|
1976
|
+
type: eventType,
|
|
1977
|
+
payload: pruneUndefinedJsonObject({
|
|
1978
|
+
subjectId: options.job ? swarmRunTaskNodeId(options.job.taskId) : undefined,
|
|
1979
|
+
lease: leaseNode
|
|
1980
|
+
})
|
|
1981
|
+
})];
|
|
1982
|
+
}
|
|
1983
|
+
export function createRunEventsFromSwarmResult(resultInput, options = {}) {
|
|
1984
|
+
const result = isSwarmJobResult(resultInput) ? cloneJsonValue(resultInput) : normalizeResult(resultInput);
|
|
1985
|
+
const job = options.job;
|
|
1986
|
+
const runId = options.runId ?? 'frontier-swarm';
|
|
1987
|
+
const actorId = options.actorId ?? 'frontier-swarm-worker';
|
|
1988
|
+
const time = swarmRunEventTime(options, result.finishedAt ?? result.startedAt);
|
|
1989
|
+
let actorSeq = options.startActorSeq ?? 1;
|
|
1990
|
+
const parents = [...(options.parents ?? [])];
|
|
1991
|
+
const events = [];
|
|
1992
|
+
const attemptId = swarmRunAttemptNodeId(result.jobId);
|
|
1993
|
+
const taskId = swarmRunTaskNodeId(job?.taskId ?? result.jobId);
|
|
1994
|
+
const attemptEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunAttempt({
|
|
1995
|
+
id: attemptId,
|
|
1996
|
+
title: job?.title ?? result.jobId,
|
|
1997
|
+
taskId,
|
|
1998
|
+
actorId,
|
|
1999
|
+
runnerId: job?.compute.kind ?? job?.compute.id,
|
|
2000
|
+
workspaceId: job?.worktreePath,
|
|
2001
|
+
status: swarmResultStatusToAttemptStatus(result.status),
|
|
2002
|
+
startedAt: result.startedAt ? new Date(result.startedAt).toISOString() : undefined,
|
|
2003
|
+
endedAt: result.finishedAt ? new Date(result.finishedAt).toISOString() : undefined,
|
|
2004
|
+
model: job?.compute.model,
|
|
2005
|
+
reason: result.error,
|
|
2006
|
+
metadata: pruneUndefinedJsonObject({
|
|
2007
|
+
source: 'frontier-swarm.result',
|
|
2008
|
+
jobId: result.jobId,
|
|
2009
|
+
mergeReadiness: result.mergeReadiness,
|
|
2010
|
+
mergeDisposition: result.mergeDisposition,
|
|
2011
|
+
riskLevel: result.riskLevel,
|
|
2012
|
+
exitCode: result.exitCode,
|
|
2013
|
+
signal: result.signal,
|
|
2014
|
+
queueItemIds: result.queueItemIds,
|
|
2015
|
+
semanticImport: result.semanticImport,
|
|
2016
|
+
metadata: result.metadata
|
|
2017
|
+
})
|
|
2018
|
+
}), { parents, time });
|
|
2019
|
+
events.push(attemptEvent);
|
|
2020
|
+
let latestParents = [attemptEvent.id];
|
|
2021
|
+
if (result.patchPath || result.changedPaths.length) {
|
|
2022
|
+
const patchId = swarmRunPatchNodeId(result.jobId);
|
|
2023
|
+
const patchEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunPatch({
|
|
2024
|
+
id: patchId,
|
|
2025
|
+
title: `Patch ${result.jobId}`,
|
|
2026
|
+
changedPaths: result.changedPaths,
|
|
2027
|
+
artifactId: result.patchPath ? swarmRunArtifactNodeId(result.patchPath) : undefined,
|
|
2028
|
+
summary: result.lastMessage,
|
|
2029
|
+
risk: swarmRiskToRunRisk(result.riskLevel),
|
|
2030
|
+
metadata: pruneUndefinedJsonObject({
|
|
2031
|
+
source: 'frontier-swarm.result.patch',
|
|
2032
|
+
patchPath: result.patchPath,
|
|
2033
|
+
changedRegions: result.changedRegions,
|
|
2034
|
+
ownershipViolations: result.ownershipViolations
|
|
2035
|
+
})
|
|
2036
|
+
}), { parents: latestParents, time });
|
|
2037
|
+
events.push(patchEvent);
|
|
2038
|
+
events.push(createRunEdgeEvent(runId, actorId, actorSeq++, linkRunNodes(attemptId, patchId, 'produces-patch', { createdAt: time }), {
|
|
2039
|
+
parents: [patchEvent.id],
|
|
2040
|
+
time
|
|
2041
|
+
}));
|
|
2042
|
+
if (result.patchPath) {
|
|
2043
|
+
events.push(createRunEvent({
|
|
2044
|
+
runId,
|
|
2045
|
+
actorId,
|
|
2046
|
+
actorSeq: actorSeq++,
|
|
2047
|
+
parents: [patchEvent.id],
|
|
2048
|
+
time,
|
|
2049
|
+
type: 'artifact.attached',
|
|
2050
|
+
payload: {
|
|
2051
|
+
subjectId: patchId,
|
|
2052
|
+
artifact: toJsonValue(defineRunArtifact({
|
|
2053
|
+
id: swarmRunArtifactNodeId(result.patchPath),
|
|
2054
|
+
title: result.patchPath,
|
|
2055
|
+
artifactType: 'patch',
|
|
2056
|
+
path: result.patchPath,
|
|
2057
|
+
summary: `Patch artifact for ${result.jobId}`,
|
|
2058
|
+
metadata: { source: 'frontier-swarm.result.patch-artifact', jobId: result.jobId }
|
|
2059
|
+
}))
|
|
2060
|
+
}
|
|
2061
|
+
}));
|
|
2062
|
+
}
|
|
2063
|
+
latestParents = [patchEvent.id];
|
|
2064
|
+
}
|
|
2065
|
+
for (const evidencePath of result.evidencePaths) {
|
|
2066
|
+
const artifactId = swarmRunArtifactNodeId(evidencePath);
|
|
2067
|
+
const evidenceId = swarmRunEvidenceNodeId(result.jobId, evidencePath);
|
|
2068
|
+
const artifactEvent = createRunEvent({
|
|
2069
|
+
runId,
|
|
2070
|
+
actorId,
|
|
2071
|
+
actorSeq: actorSeq++,
|
|
2072
|
+
parents: latestParents,
|
|
2073
|
+
time,
|
|
2074
|
+
type: 'artifact.attached',
|
|
2075
|
+
payload: {
|
|
2076
|
+
subjectId: attemptId,
|
|
2077
|
+
artifact: toJsonValue(defineRunArtifact({
|
|
2078
|
+
id: artifactId,
|
|
2079
|
+
title: evidencePath,
|
|
2080
|
+
artifactType: inferSwarmArtifactType(evidencePath),
|
|
2081
|
+
path: evidencePath,
|
|
2082
|
+
summary: `Evidence artifact for ${result.jobId}`,
|
|
2083
|
+
metadata: { source: 'frontier-swarm.result.evidence-artifact', jobId: result.jobId }
|
|
2084
|
+
}))
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
events.push(artifactEvent);
|
|
2088
|
+
const evidenceEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunEvidence({
|
|
2089
|
+
id: evidenceId,
|
|
2090
|
+
title: evidencePath,
|
|
2091
|
+
evidenceType: inferSwarmArtifactType(evidencePath),
|
|
2092
|
+
result: result.status === 'completed' || result.status === 'verified' ? 'pass' : result.status === 'failed' ? 'fail' : 'unknown',
|
|
2093
|
+
artifactIds: [artifactId],
|
|
2094
|
+
summary: `Evidence from ${result.jobId}`,
|
|
2095
|
+
metadata: { source: 'frontier-swarm.result.evidence', jobId: result.jobId }
|
|
2096
|
+
}), { parents: [artifactEvent.id], time });
|
|
2097
|
+
events.push(evidenceEvent);
|
|
2098
|
+
events.push(createRunEdgeEvent(runId, actorId, actorSeq++, linkRunNodes(attemptId, evidenceId, 'produces-evidence', { createdAt: time }), {
|
|
2099
|
+
parents: [evidenceEvent.id],
|
|
2100
|
+
time
|
|
2101
|
+
}));
|
|
2102
|
+
}
|
|
2103
|
+
for (let index = 0; index < result.verification.length; index += 1) {
|
|
2104
|
+
const verification = result.verification[index];
|
|
2105
|
+
const verificationId = swarmRunVerificationNodeId(result.jobId, index, verification.name);
|
|
2106
|
+
const verificationEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunVerification({
|
|
2107
|
+
id: verificationId,
|
|
2108
|
+
title: verification.name,
|
|
2109
|
+
status: verification.status === 0 ? 'passed' : verification.required === false && verification.status !== 0 ? 'skipped' : 'failed',
|
|
2110
|
+
command: verification.command[0],
|
|
2111
|
+
args: verification.command.slice(1),
|
|
2112
|
+
cwd: verification.cwd,
|
|
2113
|
+
exitCode: verification.status,
|
|
2114
|
+
artifactIds: [],
|
|
2115
|
+
required: verification.required,
|
|
2116
|
+
summary: verification.commandLine,
|
|
2117
|
+
metadata: pruneUndefinedJsonObject({
|
|
2118
|
+
source: 'frontier-swarm.result.verification',
|
|
2119
|
+
jobId: result.jobId,
|
|
2120
|
+
durationMs: verification.durationMs,
|
|
2121
|
+
stdoutTail: verification.stdoutTail,
|
|
2122
|
+
stderrTail: verification.stderrTail,
|
|
2123
|
+
category: verification.category,
|
|
2124
|
+
metadata: verification.metadata
|
|
2125
|
+
})
|
|
2126
|
+
}), { parents: latestParents, time });
|
|
2127
|
+
events.push(verificationEvent);
|
|
2128
|
+
events.push(createRunEdgeEvent(runId, actorId, actorSeq++, linkRunNodes(attemptId, verificationId, 'verified-by', { createdAt: time }), {
|
|
2129
|
+
parents: [verificationEvent.id],
|
|
2130
|
+
time
|
|
2131
|
+
}));
|
|
2132
|
+
}
|
|
2133
|
+
return events;
|
|
2134
|
+
}
|
|
2135
|
+
export function createRunEventsFromMergeBundle(bundle, options = {}) {
|
|
2136
|
+
const runId = options.runId ?? bundle.runId ?? 'frontier-swarm';
|
|
2137
|
+
const actorId = options.actorId ?? 'frontier-swarm-collector';
|
|
2138
|
+
const time = swarmRunEventTime(options, bundle.generatedAt);
|
|
2139
|
+
let actorSeq = options.startActorSeq ?? 1;
|
|
2140
|
+
const parents = [...(options.parents ?? [])];
|
|
2141
|
+
const events = [];
|
|
2142
|
+
const patchId = swarmRunPatchNodeId(bundle.jobId);
|
|
2143
|
+
const patchEvent = createRunNodeEvent(runId, actorId, actorSeq++, defineRunPatch({
|
|
2144
|
+
id: patchId,
|
|
2145
|
+
title: bundle.title ?? `Merge bundle ${bundle.jobId}`,
|
|
2146
|
+
changedPaths: bundle.changedPaths,
|
|
2147
|
+
artifactId: bundle.patchPath ? swarmRunArtifactNodeId(bundle.patchPath) : undefined,
|
|
2148
|
+
summary: bundle.reasons.join('; '),
|
|
2149
|
+
risk: swarmRiskToRunRisk(bundle.riskLevel),
|
|
2150
|
+
metadata: pruneUndefinedJsonObject({
|
|
2151
|
+
source: 'frontier-swarm.merge-bundle',
|
|
2152
|
+
bundleId: bundle.id,
|
|
2153
|
+
jobId: bundle.jobId,
|
|
2154
|
+
taskId: bundle.taskId,
|
|
2155
|
+
lane: bundle.lane,
|
|
2156
|
+
mergeReadiness: bundle.mergeReadiness,
|
|
2157
|
+
disposition: bundle.disposition,
|
|
2158
|
+
autoMergeable: bundle.autoMergeable,
|
|
2159
|
+
staleAgainstHead: bundle.staleAgainstHead,
|
|
2160
|
+
changedRegions: bundle.changedRegions,
|
|
2161
|
+
ownedFilesTouched: bundle.ownedFilesTouched,
|
|
2162
|
+
ownershipViolations: bundle.ownershipViolations,
|
|
2163
|
+
patchHash: bundle.patchHash,
|
|
2164
|
+
branchName: bundle.branchName,
|
|
2165
|
+
commit: bundle.commit,
|
|
2166
|
+
semanticImport: bundle.semanticImport
|
|
2167
|
+
})
|
|
2168
|
+
}), { parents, time });
|
|
2169
|
+
events.push(patchEvent);
|
|
2170
|
+
if (bundle.patchPath) {
|
|
2171
|
+
events.push(createRunEvent({
|
|
2172
|
+
runId,
|
|
2173
|
+
actorId,
|
|
2174
|
+
actorSeq: actorSeq++,
|
|
2175
|
+
parents: [patchEvent.id],
|
|
2176
|
+
time,
|
|
2177
|
+
type: 'artifact.attached',
|
|
2178
|
+
payload: {
|
|
2179
|
+
subjectId: patchId,
|
|
2180
|
+
artifact: toJsonValue(defineRunArtifact({
|
|
2181
|
+
id: swarmRunArtifactNodeId(bundle.patchPath),
|
|
2182
|
+
title: bundle.patchPath,
|
|
2183
|
+
artifactType: 'patch',
|
|
2184
|
+
path: bundle.patchPath,
|
|
2185
|
+
hash: bundle.patchHash,
|
|
2186
|
+
summary: `Merge patch for ${bundle.jobId}`,
|
|
2187
|
+
metadata: { source: 'frontier-swarm.merge-bundle.patch', bundleId: bundle.id }
|
|
2188
|
+
}))
|
|
2189
|
+
}
|
|
2190
|
+
}));
|
|
2191
|
+
}
|
|
2192
|
+
for (const evidencePath of bundle.evidencePaths) {
|
|
2193
|
+
events.push(createRunEvent({
|
|
2194
|
+
runId,
|
|
2195
|
+
actorId,
|
|
2196
|
+
actorSeq: actorSeq++,
|
|
2197
|
+
parents: [patchEvent.id],
|
|
2198
|
+
time,
|
|
2199
|
+
type: 'artifact.attached',
|
|
2200
|
+
payload: {
|
|
2201
|
+
subjectId: patchId,
|
|
2202
|
+
artifact: toJsonValue(defineRunArtifact({
|
|
2203
|
+
id: swarmRunArtifactNodeId(evidencePath),
|
|
2204
|
+
title: evidencePath,
|
|
2205
|
+
artifactType: inferSwarmArtifactType(evidencePath),
|
|
2206
|
+
path: evidencePath,
|
|
2207
|
+
summary: `Merge evidence for ${bundle.jobId}`,
|
|
2208
|
+
metadata: { source: 'frontier-swarm.merge-bundle.evidence', bundleId: bundle.id }
|
|
2209
|
+
}))
|
|
2210
|
+
}
|
|
2211
|
+
}));
|
|
2212
|
+
}
|
|
2213
|
+
const decisionKind = swarmDispositionToRunDecision(bundle.disposition, bundle.mergeReadiness);
|
|
2214
|
+
const decisionEvent = createRunEvent({
|
|
2215
|
+
runId,
|
|
2216
|
+
actorId,
|
|
2217
|
+
actorSeq: actorSeq++,
|
|
2218
|
+
parents: [patchEvent.id],
|
|
2219
|
+
time,
|
|
2220
|
+
type: 'decision.recorded',
|
|
2221
|
+
payload: {
|
|
2222
|
+
decision: toJsonValue(defineRunDecision({
|
|
2223
|
+
id: swarmRunDecisionNodeId(bundle.id || bundle.jobId),
|
|
2224
|
+
title: `Merge decision ${bundle.jobId}`,
|
|
2225
|
+
decision: decisionKind,
|
|
2226
|
+
subjectIds: uniqueStrings([patchId, bundle.taskId ? swarmRunTaskNodeId(bundle.taskId) : undefined]),
|
|
2227
|
+
actorId,
|
|
2228
|
+
reason: bundle.reasons.join('; ') || bundle.disposition,
|
|
2229
|
+
requiredActions: bundle.disposition === 'needs-port' ? ['human-port'] : bundle.staleAgainstHead ? ['rerun-against-head'] : [],
|
|
2230
|
+
metadata: pruneUndefinedJsonObject({
|
|
2231
|
+
source: 'frontier-swarm.merge-bundle.decision',
|
|
2232
|
+
bundleId: bundle.id,
|
|
2233
|
+
disposition: bundle.disposition,
|
|
2234
|
+
mergeReadiness: bundle.mergeReadiness,
|
|
2235
|
+
riskLevel: bundle.riskLevel,
|
|
2236
|
+
autoMergeable: bundle.autoMergeable
|
|
2237
|
+
})
|
|
2238
|
+
}))
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
events.push(decisionEvent);
|
|
2242
|
+
return events;
|
|
2243
|
+
}
|
|
2244
|
+
export function createRunEventsFromCoordinatorDecision(decision, options = {}) {
|
|
2245
|
+
const normalized = createSwarmQueueOutcomeDecision(decision);
|
|
2246
|
+
const runId = options.runId ?? 'frontier-swarm';
|
|
2247
|
+
const actorId = options.actorId ?? 'frontier-swarm-coordinator';
|
|
2248
|
+
const time = swarmRunEventTime(options, normalized.generatedAt);
|
|
2249
|
+
const subjectIds = uniqueStrings([
|
|
2250
|
+
normalized.jobId ? swarmRunAttemptNodeId(normalized.jobId) : undefined,
|
|
2251
|
+
normalized.taskId ? swarmRunTaskNodeId(normalized.taskId) : undefined,
|
|
2252
|
+
normalized.subjectId
|
|
2253
|
+
]);
|
|
2254
|
+
return [createRunEvent({
|
|
2255
|
+
runId,
|
|
2256
|
+
actorId,
|
|
2257
|
+
actorSeq: options.startActorSeq ?? 1,
|
|
2258
|
+
parents: [...(options.parents ?? [])],
|
|
2259
|
+
time,
|
|
2260
|
+
type: 'decision.recorded',
|
|
2261
|
+
payload: {
|
|
2262
|
+
decision: toJsonValue(defineRunDecision({
|
|
2263
|
+
id: swarmRunDecisionNodeId(normalized.id),
|
|
2264
|
+
title: `Coordinator decision ${normalized.subjectId}`,
|
|
2265
|
+
decision: swarmQueueOutcomeToRunDecision(normalized),
|
|
2266
|
+
subjectIds,
|
|
2267
|
+
actorId,
|
|
2268
|
+
reason: normalized.reasons.join('; ') || normalized.outcome,
|
|
2269
|
+
requiredActions: swarmQueueOutcomeRequiredActions(normalized),
|
|
2270
|
+
metadata: pruneUndefinedJsonObject({
|
|
2271
|
+
source: 'frontier-swarm.queue-outcome-decision',
|
|
2272
|
+
decision: normalized.decision,
|
|
2273
|
+
action: normalized.action,
|
|
2274
|
+
assignedAction: normalized.assignedAction,
|
|
2275
|
+
category: normalized.category,
|
|
2276
|
+
outcome: normalized.outcome,
|
|
2277
|
+
terminal: normalized.terminal,
|
|
2278
|
+
queueItemIds: normalized.queueItemIds,
|
|
2279
|
+
queueId: normalized.queueId,
|
|
2280
|
+
lane: normalized.lane,
|
|
2281
|
+
disposition: normalized.disposition,
|
|
2282
|
+
mergeReadiness: normalized.mergeReadiness,
|
|
2283
|
+
status: normalized.status,
|
|
2284
|
+
conflictingJobIds: normalized.conflictingJobIds,
|
|
2285
|
+
metadata: normalized.metadata
|
|
2286
|
+
})
|
|
2287
|
+
}))
|
|
2288
|
+
}
|
|
2289
|
+
})];
|
|
2290
|
+
}
|
|
2291
|
+
export function createRunProjectionFromSwarmRunEvents(events, options = {}) {
|
|
2292
|
+
return replayRunEvents(events, {
|
|
2293
|
+
id: options.runId,
|
|
2294
|
+
goal: options.goal,
|
|
2295
|
+
metadata: toJsonObject(options.metadata)
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
export function createRunDashboardFromSwarmRun(input, options = {}) {
|
|
2299
|
+
const events = isFrontierRunEventList(input) ? input : createRunEventsFromSwarmRun(input, options);
|
|
2300
|
+
return createRunDashboardSnapshot(createRunProjectionFromSwarmRunEvents(events, options));
|
|
2301
|
+
}
|
|
2302
|
+
export function createRunEventsFromSwarmRun(run, options = {}) {
|
|
2303
|
+
const runId = options.runId ?? run.id;
|
|
2304
|
+
const actorId = options.actorId ?? 'frontier-swarm';
|
|
2305
|
+
const created = createRunEvent({
|
|
2306
|
+
runId,
|
|
2307
|
+
actorId,
|
|
2308
|
+
actorSeq: options.startActorSeq ?? 1,
|
|
2309
|
+
parents: [...(options.parents ?? [])],
|
|
2310
|
+
time: swarmRunEventTime(options, run.startedAt),
|
|
2311
|
+
type: 'run.created',
|
|
2312
|
+
payload: {
|
|
2313
|
+
goal: run.metadata?.objective ?? run.metadata?.goal ?? `Frontier swarm ${run.id}`,
|
|
2314
|
+
metadata: pruneUndefinedJsonObject({
|
|
2315
|
+
source: 'frontier-swarm.run',
|
|
2316
|
+
planId: run.planId,
|
|
2317
|
+
manifestId: run.manifestId,
|
|
2318
|
+
status: run.status,
|
|
2319
|
+
summary: run.summary,
|
|
2320
|
+
metadata: run.metadata
|
|
2321
|
+
})
|
|
2322
|
+
}
|
|
2323
|
+
});
|
|
2324
|
+
return [
|
|
2325
|
+
created,
|
|
2326
|
+
...run.results.flatMap((result, index) => createRunEventsFromSwarmResult(result, {
|
|
2327
|
+
runId,
|
|
2328
|
+
actorId: 'frontier-swarm-worker',
|
|
2329
|
+
startActorSeq: 1000 + index * 100,
|
|
2330
|
+
parents: [created.id],
|
|
2331
|
+
job: run.jobs.find((job) => job.id === result.jobId)
|
|
2332
|
+
}))
|
|
2333
|
+
];
|
|
752
2334
|
}
|
|
753
2335
|
export function recordSwarmEvent(runInput, eventInput) {
|
|
754
2336
|
const run = cloneJsonValue(runInput);
|
|
@@ -1030,6 +2612,14 @@ const SEMANTIC_IMPORT_CONFLICT_HINT_KEYS = new Set([
|
|
|
1030
2612
|
'symbols',
|
|
1031
2613
|
'touchedSymbols'
|
|
1032
2614
|
]);
|
|
2615
|
+
const SEMANTIC_IMPORT_PUBLIC_CONTRACT_HINT_KEYS = new Set([
|
|
2616
|
+
'apiContract',
|
|
2617
|
+
'apiContracts',
|
|
2618
|
+
'publicContract',
|
|
2619
|
+
'publicContractId',
|
|
2620
|
+
'publicContractIds',
|
|
2621
|
+
'publicContracts'
|
|
2622
|
+
]);
|
|
1033
2623
|
function semanticImportExportMetadata(value, key, file) {
|
|
1034
2624
|
const explicitKind = semanticImportStringValue(value.kind);
|
|
1035
2625
|
const declarationKind = semanticImportStringValue(value.declarationKind)
|
|
@@ -1119,9 +2709,28 @@ function semanticImportStringValue(value) {
|
|
|
1119
2709
|
function semanticMergeConflictKeys(semanticImport) {
|
|
1120
2710
|
const records = semanticImportRecords(semanticImport);
|
|
1121
2711
|
const sources = records.length > 0 ? records : [semanticImport];
|
|
1122
|
-
return uniqueStrings(sources.flatMap((source) =>
|
|
2712
|
+
return uniqueStrings(sources.flatMap((source) => semanticImportConflictKeyStringsForValue('mergeCandidate' in source && source.mergeCandidate !== undefined
|
|
1123
2713
|
? source.mergeCandidate
|
|
1124
|
-
: source)
|
|
2714
|
+
: source)));
|
|
2715
|
+
}
|
|
2716
|
+
function semanticImportConflictKeyStringsForValue(value, key, depth = 0) {
|
|
2717
|
+
if (value === undefined || value === null)
|
|
2718
|
+
return [];
|
|
2719
|
+
if (typeof value === 'string') {
|
|
2720
|
+
const normalized = value.trim();
|
|
2721
|
+
if (!normalized)
|
|
2722
|
+
return [];
|
|
2723
|
+
if (key && SEMANTIC_IMPORT_PUBLIC_CONTRACT_HINT_KEYS.has(key))
|
|
2724
|
+
return [`public-contract:${publicContractConflictValue(normalized)}`];
|
|
2725
|
+
if (key && SEMANTIC_IMPORT_CONFLICT_HINT_KEYS.has(key))
|
|
2726
|
+
return [`symbol:${normalized}`];
|
|
2727
|
+
return [];
|
|
2728
|
+
}
|
|
2729
|
+
if (depth >= 4 || typeof value !== 'object')
|
|
2730
|
+
return [];
|
|
2731
|
+
if (Array.isArray(value))
|
|
2732
|
+
return value.flatMap((item) => semanticImportConflictKeyStringsForValue(item, key, depth + 1));
|
|
2733
|
+
return Object.entries(value).flatMap(([nextKey, nextValue]) => (semanticImportConflictKeyStringsForValue(nextValue, nextKey, depth + 1)));
|
|
1125
2734
|
}
|
|
1126
2735
|
function semanticImportConflictHintStringsForValue(value, key, depth = 0) {
|
|
1127
2736
|
if (value === undefined || value === null)
|
|
@@ -2467,6 +4076,11 @@ export function createSwarmHierarchicalMergeQueue(input) {
|
|
|
2467
4076
|
const admissionPressure = summarizeSwarmMergeAdmissionPressure(assignments);
|
|
2468
4077
|
const orderedScopes = Array.from(scopes.values()).sort((left, right) => (mergeQueueScopeRank(left.kind) - mergeQueueScopeRank(right.kind)
|
|
2469
4078
|
|| left.id.localeCompare(right.id)));
|
|
4079
|
+
const semanticLeaseScopeMap = createSwarmSemanticLeaseScopeMap(orderedScopes, {
|
|
4080
|
+
rootScopeId,
|
|
4081
|
+
...semanticLeaseScopeOptionsFromMetadata(input.metadata)
|
|
4082
|
+
});
|
|
4083
|
+
const scopesWithSemanticLeases = orderedScopes.map((scope) => attachSemanticLeaseScopeToMergeQueueScope(scope, semanticLeaseScopeMap));
|
|
2470
4084
|
const queueId = input.id ?? 'swarm-hierarchical-merge-queue:' + stableHash([input.index.id, input.admission?.id, orderedScopes, assignments, promotions, generatedAt]);
|
|
2471
4085
|
const linkedAssignments = assignments.map((assignment) => {
|
|
2472
4086
|
if (!coordinatorAgentDrainActionIsTerminal(assignment.action))
|
|
@@ -2476,19 +4090,19 @@ export function createSwarmHierarchicalMergeQueue(input) {
|
|
|
2476
4090
|
terminalDecisionId: hierarchicalQueueTerminalDecisionId(queueId, assignment),
|
|
2477
4091
|
terminalDecisionQueueItemIds: [...assignment.queueItemIds]
|
|
2478
4092
|
};
|
|
2479
|
-
});
|
|
4093
|
+
}).map((assignment) => attachSemanticLeaseScopesToAssignment(assignment, semanticLeaseScopeMap));
|
|
2480
4094
|
const leaseRecords = createHierarchicalQueueLeaseRecords({
|
|
2481
4095
|
queueId,
|
|
2482
4096
|
rootScopeId,
|
|
2483
4097
|
generatedAt,
|
|
2484
|
-
scopes:
|
|
4098
|
+
scopes: scopesWithSemanticLeases,
|
|
2485
4099
|
assignments: linkedAssignments,
|
|
2486
4100
|
promotions,
|
|
2487
4101
|
localLeader: input.localLeader,
|
|
2488
4102
|
localLeaders: input.localLeaders
|
|
2489
4103
|
});
|
|
2490
4104
|
const byLeaseKey = groupJobIdsByMany(linkedAssignments, (assignment) => assignment.requiredLeaseKeys ?? [assignment.leaseKey]);
|
|
2491
|
-
const scopeTree = createHierarchicalQueueScopeTree(
|
|
4105
|
+
const scopeTree = createHierarchicalQueueScopeTree(scopesWithSemanticLeases, rootScopeId);
|
|
2492
4106
|
return {
|
|
2493
4107
|
kind: FRONTIER_SWARM_HIERARCHICAL_MERGE_QUEUE_KIND,
|
|
2494
4108
|
version: FRONTIER_SWARM_HIERARCHICAL_MERGE_QUEUE_VERSION,
|
|
@@ -2498,13 +4112,14 @@ export function createSwarmHierarchicalMergeQueue(input) {
|
|
|
2498
4112
|
generatedAt,
|
|
2499
4113
|
rootScopeId,
|
|
2500
4114
|
scopeTree,
|
|
2501
|
-
scopes:
|
|
4115
|
+
scopes: scopesWithSemanticLeases,
|
|
2502
4116
|
leaseRecords,
|
|
2503
4117
|
assignments: linkedAssignments,
|
|
2504
4118
|
promotions,
|
|
2505
4119
|
byScope,
|
|
2506
4120
|
byLeaseKey,
|
|
2507
4121
|
byAction,
|
|
4122
|
+
semanticLeaseScopes: uniqueSemanticLeaseScopes(Array.from(semanticLeaseScopeMap.values())),
|
|
2508
4123
|
summary: {
|
|
2509
4124
|
scopeCount: orderedScopes.length,
|
|
2510
4125
|
assignmentCount: linkedAssignments.length,
|
|
@@ -2534,6 +4149,7 @@ export function createSwarmCoordinatorAgentDrainWork(input) {
|
|
|
2534
4149
|
title: scope.title,
|
|
2535
4150
|
leaseScope: scope.leaseKey,
|
|
2536
4151
|
leaseKey: scope.leaseKey,
|
|
4152
|
+
...(scope.semanticLeaseScope ? { semanticLeaseScope: scope.semanticLeaseScope } : {}),
|
|
2537
4153
|
...(scope.parentId ? { parentQueueId: scope.parentId } : {}),
|
|
2538
4154
|
...(scope.lane ? { lane: scope.lane } : {}),
|
|
2539
4155
|
changedPaths: [...scope.changedPaths],
|
|
@@ -2563,6 +4179,9 @@ export function createSwarmCoordinatorAgentDrainWork(input) {
|
|
|
2563
4179
|
const requiredLeaseKeys = assignmentRequiredLeaseKeys.length
|
|
2564
4180
|
? [...assignmentRequiredLeaseKeys]
|
|
2565
4181
|
: [leaseScope];
|
|
4182
|
+
const requiredSemanticLeaseScopes = requiredLeaseScopeIds
|
|
4183
|
+
.map((scopeId) => scopesById.get(scopeId)?.semanticLeaseScope)
|
|
4184
|
+
.filter((scope) => Boolean(scope));
|
|
2566
4185
|
const decision = coordinatorAgentDrainDecisionForAction(assignment.action);
|
|
2567
4186
|
const terminal = coordinatorAgentDrainActionIsTerminal(assignment.action);
|
|
2568
4187
|
const parentQueueId = assignment.action === 'promote'
|
|
@@ -2599,6 +4218,7 @@ export function createSwarmCoordinatorAgentDrainWork(input) {
|
|
|
2599
4218
|
conflictingJobIds: [...assignment.conflictingJobIds],
|
|
2600
4219
|
requiredLeaseScopeIds,
|
|
2601
4220
|
requiredLeaseKeys,
|
|
4221
|
+
...(requiredSemanticLeaseScopes.length ? { requiredSemanticLeaseScopes } : {}),
|
|
2602
4222
|
...(assignment.retrySlices?.length ? { retrySlices: cloneMergeQueueRetrySlices(assignment.retrySlices) } : {}),
|
|
2603
4223
|
...(assignment.semanticSliceScopeIds?.length ? { semanticSliceScopeIds: [...assignment.semanticSliceScopeIds] } : {}),
|
|
2604
4224
|
...(assignment.semanticSliceLeaseKeys?.length ? { semanticSliceLeaseKeys: [...assignment.semanticSliceLeaseKeys] } : {}),
|
|
@@ -2623,6 +4243,7 @@ export function createSwarmCoordinatorAgentDrainWork(input) {
|
|
|
2623
4243
|
reasons: [...assignment.reasons],
|
|
2624
4244
|
requiredLeaseScopeIds: [...(assignment.requiredLeaseScopeIds ?? [])],
|
|
2625
4245
|
requiredLeaseKeys: [...(assignment.requiredLeaseKeys ?? [])],
|
|
4246
|
+
...(assignment.requiredSemanticLeaseScopes?.length ? { requiredSemanticLeaseScopes: assignment.requiredSemanticLeaseScopes.map(cloneSemanticLeaseScope) } : {}),
|
|
2626
4247
|
...(assignment.retrySlices?.length ? { retrySlices: cloneMergeQueueRetrySlices(assignment.retrySlices) } : {}),
|
|
2627
4248
|
...(assignment.semanticSliceScopeIds?.length ? { semanticSliceScopeIds: [...assignment.semanticSliceScopeIds] } : {}),
|
|
2628
4249
|
...(assignment.semanticSliceLeaseKeys?.length ? { semanticSliceLeaseKeys: [...assignment.semanticSliceLeaseKeys] } : {}),
|
|
@@ -2649,6 +4270,7 @@ export function createSwarmCoordinatorAgentDrainWork(input) {
|
|
|
2649
4270
|
reasons: [...assignment.reasons],
|
|
2650
4271
|
requiredLeaseScopeIds: [...(assignment.requiredLeaseScopeIds ?? [])],
|
|
2651
4272
|
requiredLeaseKeys: [...(assignment.requiredLeaseKeys ?? [])],
|
|
4273
|
+
...(assignment.requiredSemanticLeaseScopes?.length ? { requiredSemanticLeaseScopes: assignment.requiredSemanticLeaseScopes.map(cloneSemanticLeaseScope) } : {}),
|
|
2652
4274
|
...(assignment.metadata ? { metadata: cloneJsonValue(assignment.metadata) } : {})
|
|
2653
4275
|
}));
|
|
2654
4276
|
const activeAssignments = assignments.filter((assignment) => !coordinatorAgentDrainAssignmentIsTerminal(assignment));
|
|
@@ -2706,6 +4328,120 @@ export function createSwarmCoordinatorAgentDrainWork(input) {
|
|
|
2706
4328
|
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
2707
4329
|
};
|
|
2708
4330
|
}
|
|
4331
|
+
export function createSwarmSemanticLeaseScopeForMergeQueueScope(scope, input = {}) {
|
|
4332
|
+
const scopesById = new Map((input.scopes ?? []).map((entry) => [entry.id, entry]));
|
|
4333
|
+
const parentKeys = [];
|
|
4334
|
+
let parentId = scope.parentId;
|
|
4335
|
+
const visited = new Set([scope.id]);
|
|
4336
|
+
while (parentId && !visited.has(parentId)) {
|
|
4337
|
+
const parent = scopesById.get(parentId);
|
|
4338
|
+
if (!parent)
|
|
4339
|
+
break;
|
|
4340
|
+
parentKeys.push(parent.leaseKey);
|
|
4341
|
+
visited.add(parentId);
|
|
4342
|
+
parentId = parent.parentId;
|
|
4343
|
+
}
|
|
4344
|
+
const regionId = scope.changedRegions.length === 1 ? scope.changedRegions[0] : scope.id;
|
|
4345
|
+
const path = scope.changedPaths.length === 1 ? scope.changedPaths[0] : undefined;
|
|
4346
|
+
const metadata = mergeSwarmMetadata([
|
|
4347
|
+
scope.metadata,
|
|
4348
|
+
toJsonObject(input.metadata),
|
|
4349
|
+
{
|
|
4350
|
+
swarmQueueScopeId: scope.id,
|
|
4351
|
+
swarmQueueScopeKind: scope.kind,
|
|
4352
|
+
swarmLeaseKey: scope.leaseKey,
|
|
4353
|
+
changedPaths: scope.changedPaths,
|
|
4354
|
+
changedRegions: scope.changedRegions
|
|
4355
|
+
}
|
|
4356
|
+
]);
|
|
4357
|
+
return defineSemanticLeaseScope({
|
|
4358
|
+
kind: semanticLeaseScopeKindForMergeQueueScope(scope),
|
|
4359
|
+
key: scope.leaseKey,
|
|
4360
|
+
repository: input.repository,
|
|
4361
|
+
packageId: input.packageId,
|
|
4362
|
+
lane: scope.lane,
|
|
4363
|
+
path,
|
|
4364
|
+
regionId,
|
|
4365
|
+
name: semanticLeaseScopeNameForMergeQueueScope(scope),
|
|
4366
|
+
parentKeys,
|
|
4367
|
+
metadata
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
export function createSwarmSemanticLeaseScopesForMergeQueue(queue, input = {}) {
|
|
4371
|
+
const existing = queue.scopes
|
|
4372
|
+
.map((scope) => scope.semanticLeaseScope)
|
|
4373
|
+
.filter((scope) => Boolean(scope));
|
|
4374
|
+
if (existing.length)
|
|
4375
|
+
return uniqueSemanticLeaseScopes(existing);
|
|
4376
|
+
return uniqueSemanticLeaseScopes(queue.scopes.map((scope) => (createSwarmSemanticLeaseScopeForMergeQueueScope(scope, {
|
|
4377
|
+
...input,
|
|
4378
|
+
rootScopeId: input.rootScopeId ?? queue.rootScopeId,
|
|
4379
|
+
scopes: queue.scopes
|
|
4380
|
+
}))));
|
|
4381
|
+
}
|
|
4382
|
+
export function createSwarmSemanticLeaseStateForMergeQueue(queue, input = {}) {
|
|
4383
|
+
const semanticLeaseScopes = createSwarmSemanticLeaseScopesForMergeQueue(queue, input);
|
|
4384
|
+
return createSemanticLeaseState({
|
|
4385
|
+
id: input.id ?? `frontier-swarm-merge-queue:${queue.id}`,
|
|
4386
|
+
defaultTtlMs: input.defaultTtlMs,
|
|
4387
|
+
metadata: mergeSwarmMetadata([
|
|
4388
|
+
toJsonObject(input.metadata),
|
|
4389
|
+
{
|
|
4390
|
+
swarmQueueId: queue.id,
|
|
4391
|
+
swarmMergeIndexId: queue.mergeIndexId,
|
|
4392
|
+
rootScopeId: queue.rootScopeId,
|
|
4393
|
+
semanticLeaseScopeCount: semanticLeaseScopes.length,
|
|
4394
|
+
generatedAt: input.now ?? queue.generatedAt
|
|
4395
|
+
}
|
|
4396
|
+
])
|
|
4397
|
+
});
|
|
4398
|
+
}
|
|
4399
|
+
export function acquireSwarmCoordinatorSemanticLease(input) {
|
|
4400
|
+
const scopes = semanticLeaseScopesForCoordinatorAssignment(input.queue, input.assignment, input);
|
|
4401
|
+
const requiredLeaseScopeIds = coordinatorAssignmentRequiredLeaseScopeIds(input.assignment);
|
|
4402
|
+
const requiredLeaseKeys = coordinatorAssignmentRequiredLeaseKeys(input.assignment, scopes);
|
|
4403
|
+
const state = input.state ?? createSwarmSemanticLeaseStateForMergeQueue(input.queue, input);
|
|
4404
|
+
const acquireInput = {
|
|
4405
|
+
ownerId: input.ownerId,
|
|
4406
|
+
holderId: input.holderId,
|
|
4407
|
+
now: input.now,
|
|
4408
|
+
ttlMs: input.ttlMs,
|
|
4409
|
+
purpose: input.purpose ?? `frontier swarm coordinator apply: ${input.assignment.jobId}`,
|
|
4410
|
+
reason: input.reason,
|
|
4411
|
+
scopes,
|
|
4412
|
+
metadata: mergeSwarmMetadata([
|
|
4413
|
+
toJsonObject(input.metadata),
|
|
4414
|
+
{
|
|
4415
|
+
swarmQueueId: input.queue.id,
|
|
4416
|
+
swarmJobId: input.assignment.jobId,
|
|
4417
|
+
swarmTaskId: input.assignment.taskId,
|
|
4418
|
+
requiredLeaseScopeIds,
|
|
4419
|
+
requiredLeaseKeys
|
|
4420
|
+
}
|
|
4421
|
+
])
|
|
4422
|
+
};
|
|
4423
|
+
const mutation = acquireSemanticLease(state, acquireInput);
|
|
4424
|
+
return {
|
|
4425
|
+
state: mutation.state,
|
|
4426
|
+
mutation,
|
|
4427
|
+
scopes,
|
|
4428
|
+
requiredLeaseScopeIds,
|
|
4429
|
+
requiredLeaseKeys,
|
|
4430
|
+
...(mutation.lease ? { lease: mutation.lease } : {})
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4433
|
+
export function validateSwarmCoordinatorSemanticLeaseFence(input) {
|
|
4434
|
+
const requiredScopes = input.requiredSemanticLeaseScopes?.length
|
|
4435
|
+
? input.requiredSemanticLeaseScopes.map(cloneSemanticLeaseScope)
|
|
4436
|
+
: (input.assignment.requiredSemanticLeaseScopes ?? []).map(cloneSemanticLeaseScope);
|
|
4437
|
+
return validateSemanticLeaseFence(input.state, {
|
|
4438
|
+
leaseId: input.leaseId ?? input.lease?.id ?? '',
|
|
4439
|
+
token: input.token,
|
|
4440
|
+
fencingToken: input.fencingToken,
|
|
4441
|
+
now: input.now,
|
|
4442
|
+
scopes: requiredScopes.length ? requiredScopes : undefined
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
2709
4445
|
export function summarizeSwarmCoordinatorAgentDrainWork(work) {
|
|
2710
4446
|
const activeAssignments = work.assignments.filter((assignment) => !coordinatorAgentDrainAssignmentIsTerminal(assignment));
|
|
2711
4447
|
const terminalAssignments = work.assignments.filter((assignment) => coordinatorAgentDrainAssignmentIsTerminal(assignment));
|
|
@@ -2879,7 +4615,8 @@ export function classifySwarmQueueOutcome(input) {
|
|
|
2879
4615
|
|| action === 'reject'
|
|
2880
4616
|
|| action === 'record-only'
|
|
2881
4617
|
|| input.disposition === 'rejected'
|
|
2882
|
-
|| input.mergeReadiness === 'discovery-only'
|
|
4618
|
+
|| input.mergeReadiness === 'discovery-only'
|
|
4619
|
+
|| queueOutcomeHas(search, 'research-complete', 'discovery-complete', 'synthesized')) {
|
|
2883
4620
|
category = 'terminal';
|
|
2884
4621
|
}
|
|
2885
4622
|
else if (action === 'rerun'
|
|
@@ -3137,6 +4874,8 @@ function terminalOutcomeLabelFromText(value) {
|
|
|
3137
4874
|
return 'evidence-only';
|
|
3138
4875
|
if (token === 'no-change' || token === 'nochange' || token === 'no-op' || token === 'noop' || token === 'unchanged')
|
|
3139
4876
|
return 'no-change';
|
|
4877
|
+
if (token === 'research-complete' || token === 'discovery-complete' || token === 'synthesized')
|
|
4878
|
+
return 'research-complete';
|
|
3140
4879
|
if (token === 'generated-by-collector' || token === 'collector-generated' || token === 'generated-collector')
|
|
3141
4880
|
return 'generated-by-collector';
|
|
3142
4881
|
if (token === 'patch-missing' || token === 'missing-patch' || token === 'patchmissing')
|
|
@@ -3149,7 +4888,7 @@ function terminalOutcomeLabelFromText(value) {
|
|
|
3149
4888
|
return 'rejected';
|
|
3150
4889
|
if (token === 'conflict-blocked' || token === 'merge-conflict' || token === 'textual-conflict' || token === 'conflict')
|
|
3151
4890
|
return 'conflict-blocked';
|
|
3152
|
-
if (token === 'human-question' || token === 'human-blocked' || token === 'blocked')
|
|
4891
|
+
if (token === 'human-needed' || token === 'human-question' || token === 'human-blocked' || token === 'blocked')
|
|
3153
4892
|
return 'human-question';
|
|
3154
4893
|
if (token === 'coordinator-review' || token === 'needs-port' || token === 'escalated' || token === 'review')
|
|
3155
4894
|
return 'coordinator-review';
|
|
@@ -3178,6 +4917,12 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
|
|
|
3178
4917
|
|| terminalOutcomeTextMatches(input.outcome, 'no-change', 'nochange', 'no-op', 'noop', 'unchanged')
|
|
3179
4918
|
|| terminalOutcomeTextMatches(input.status, 'no-change', 'nochange', 'no-op', 'noop', 'unchanged')
|
|
3180
4919
|
|| terminalOutcomeTextMatches(input.decision, 'no-change', 'nochange', 'no-op', 'noop', 'unchanged');
|
|
4920
|
+
const researchComplete = typeof input === 'string'
|
|
4921
|
+
? terminalOutcomeTextMatches(input, 'research-complete', 'discovery-complete', 'synthesized')
|
|
4922
|
+
: terminalOutcomeTextMatches(input.label, 'research-complete', 'discovery-complete', 'synthesized')
|
|
4923
|
+
|| terminalOutcomeTextMatches(input.outcome, 'research-complete', 'discovery-complete', 'synthesized')
|
|
4924
|
+
|| terminalOutcomeTextMatches(input.status, 'research-complete', 'discovery-complete', 'synthesized')
|
|
4925
|
+
|| terminalOutcomeTextMatches(input.decision, 'research-complete', 'discovery-complete', 'synthesized');
|
|
3181
4926
|
const patchMissing = typeof input === 'string'
|
|
3182
4927
|
? terminalOutcomeTextMatches(input, 'patch-missing', 'missing-patch', 'patchmissing')
|
|
3183
4928
|
: input.patchMissing === true
|
|
@@ -3200,8 +4945,12 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
|
|
|
3200
4945
|
|| terminalOutcomeTextMatches(input.status, 'conflict-blocked', 'merge-conflict', 'textual-conflict', 'conflict')
|
|
3201
4946
|
|| terminalOutcomeTextMatches(input.decision, 'conflict-blocked', 'merge-conflict', 'textual-conflict', 'conflict');
|
|
3202
4947
|
const humanQuestion = typeof input === 'string'
|
|
3203
|
-
? terminalOutcomeTextMatches(input, 'human-question')
|
|
4948
|
+
? terminalOutcomeTextMatches(input, 'human-needed', 'human-question')
|
|
3204
4949
|
: input.humanQuestion === true
|
|
4950
|
+
|| terminalOutcomeTextMatches(input.label, 'human-needed')
|
|
4951
|
+
|| terminalOutcomeTextMatches(input.outcome, 'human-needed')
|
|
4952
|
+
|| terminalOutcomeTextMatches(input.status, 'human-needed')
|
|
4953
|
+
|| terminalOutcomeTextMatches(input.decision, 'human-needed')
|
|
3205
4954
|
|| terminalOutcomeTextMatches(input.label, 'human-question')
|
|
3206
4955
|
|| terminalOutcomeTextMatches(input.outcome, 'human-question')
|
|
3207
4956
|
|| terminalOutcomeTextMatches(input.status, 'human-question')
|
|
@@ -3227,18 +4976,20 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
|
|
|
3227
4976
|
? 'patch-missing'
|
|
3228
4977
|
: evidenceOnly
|
|
3229
4978
|
? 'evidence-only'
|
|
3230
|
-
:
|
|
3231
|
-
? '
|
|
3232
|
-
:
|
|
3233
|
-
? '
|
|
3234
|
-
:
|
|
3235
|
-
? '
|
|
3236
|
-
:
|
|
3237
|
-
? '
|
|
3238
|
-
:
|
|
3239
|
-
? '
|
|
3240
|
-
:
|
|
3241
|
-
|
|
4979
|
+
: researchComplete
|
|
4980
|
+
? 'research-complete'
|
|
4981
|
+
: noChange
|
|
4982
|
+
? 'no-change'
|
|
4983
|
+
: generatedByCollector
|
|
4984
|
+
? 'generated-by-collector'
|
|
4985
|
+
: conflictBlocked
|
|
4986
|
+
? 'conflict-blocked'
|
|
4987
|
+
: humanQuestion || humanBlocked
|
|
4988
|
+
? 'human-question'
|
|
4989
|
+
: coordinatorReview
|
|
4990
|
+
? 'coordinator-review'
|
|
4991
|
+
: explicitLabel ?? 'unknown';
|
|
4992
|
+
const category = label === 'applied' || label === 'committed' || label === 'checked' || label === 'superseded' || label === 'evidence-only' || label === 'no-change' || label === 'research-complete' || label === 'generated-by-collector'
|
|
3242
4993
|
? 'success'
|
|
3243
4994
|
: label === 'patch-missing' || label === 'bundle-missing'
|
|
3244
4995
|
? 'incomplete'
|
|
@@ -3264,6 +5015,65 @@ export function normalizeSwarmTerminalOutcome(input = {}) {
|
|
|
3264
5015
|
...(typeof input !== 'string' && toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
3265
5016
|
};
|
|
3266
5017
|
}
|
|
5018
|
+
export function createSwarmTerminalOutcomeRecord(input = {}) {
|
|
5019
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
5020
|
+
const queueItemIds = uniqueStrings([input.queueItemId, ...(input.queueItemIds ?? [])]);
|
|
5021
|
+
const subjectId = input.subjectId?.trim()
|
|
5022
|
+
|| queueItemIds[0]
|
|
5023
|
+
|| input.taskId?.trim()
|
|
5024
|
+
|| input.jobId?.trim()
|
|
5025
|
+
|| input.id?.trim()
|
|
5026
|
+
|| 'unknown-subject';
|
|
5027
|
+
const subjectAliases = uniqueStrings([
|
|
5028
|
+
input.subjectId,
|
|
5029
|
+
...(input.subjectAliases ?? []),
|
|
5030
|
+
...queueItemIds,
|
|
5031
|
+
input.taskId,
|
|
5032
|
+
input.jobId
|
|
5033
|
+
]);
|
|
5034
|
+
const outcome = normalizeSwarmTerminalOutcome(terminalOutcomeInputForRecord(input));
|
|
5035
|
+
const status = terminalOutcomeRecordStatusFromText(input.status)
|
|
5036
|
+
?? terminalOutcomeRecordStatusFromOutcome(outcome);
|
|
5037
|
+
const reasonCodes = normalizeSwarmTerminalOutcomeReasonCodes([
|
|
5038
|
+
...(input.reasonCodes ?? []),
|
|
5039
|
+
...defaultTerminalOutcomeRecordReasonCodes(status)
|
|
5040
|
+
]);
|
|
5041
|
+
const admitted = input.admitted ?? terminalOutcomeRecordStatusIsAdmitted(status);
|
|
5042
|
+
const evidenceRefs = normalizeNamedRefs(input.evidenceRefs ?? [], 'evidence');
|
|
5043
|
+
return {
|
|
5044
|
+
kind: FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_KIND,
|
|
5045
|
+
version: FRONTIER_SWARM_TERMINAL_OUTCOME_RECORD_VERSION,
|
|
5046
|
+
id: input.id ?? 'swarm-terminal-outcome-record:' + stableHash([subjectId, subjectAliases, status, reasonCodes, generatedAt]),
|
|
5047
|
+
generatedAt,
|
|
5048
|
+
subjectId,
|
|
5049
|
+
subjectAliases: subjectAliases.length ? subjectAliases : [subjectId],
|
|
5050
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
5051
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
5052
|
+
...(input.queueId ? { queueId: input.queueId } : {}),
|
|
5053
|
+
queueItemIds,
|
|
5054
|
+
...(input.lane ? { lane: input.lane } : {}),
|
|
5055
|
+
...(input.source ? { source: input.source } : {}),
|
|
5056
|
+
status,
|
|
5057
|
+
outcome,
|
|
5058
|
+
terminal: true,
|
|
5059
|
+
admitted,
|
|
5060
|
+
success: outcome.success,
|
|
5061
|
+
rejected: status === 'rejected',
|
|
5062
|
+
conflict: status === 'conflict',
|
|
5063
|
+
humanNeeded: status === 'human-needed',
|
|
5064
|
+
rerun: status === 'rerun',
|
|
5065
|
+
noChange: status === 'no-change',
|
|
5066
|
+
researchComplete: status === 'research-complete',
|
|
5067
|
+
reasonCodes,
|
|
5068
|
+
reasons: uniqueStrings(input.reasons ?? []),
|
|
5069
|
+
conflictingJobIds: uniqueStrings(input.conflictingJobIds ?? []),
|
|
5070
|
+
supersedes: uniqueStrings(input.supersedes ?? []),
|
|
5071
|
+
...(input.supersededBy ? { supersededBy: input.supersededBy } : {}),
|
|
5072
|
+
...(input.rerunOf ? { rerunOf: input.rerunOf } : {}),
|
|
5073
|
+
evidenceRefs,
|
|
5074
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
5075
|
+
};
|
|
5076
|
+
}
|
|
3267
5077
|
export function reconcileSwarmTerminalState(input = {}) {
|
|
3268
5078
|
const generatedAt = input.generatedAt ?? Date.now();
|
|
3269
5079
|
const doneBucket = input.doneBucket ?? 'done';
|
|
@@ -4054,13 +5864,25 @@ function createMergeIndexConflicts(entries) {
|
|
|
4054
5864
|
continue;
|
|
4055
5865
|
const keys = pairConflictKeys(left, right);
|
|
4056
5866
|
for (const key of keys) {
|
|
4057
|
-
const kind = key.startsWith('symbol:')
|
|
5867
|
+
const kind = key.startsWith('symbol:')
|
|
5868
|
+
? 'symbol'
|
|
5869
|
+
: key.startsWith('public-contract:')
|
|
5870
|
+
? 'public-contract'
|
|
5871
|
+
: key.startsWith('region:')
|
|
5872
|
+
? 'region'
|
|
5873
|
+
: 'path';
|
|
4058
5874
|
const value = key.slice(key.indexOf(':') + 1);
|
|
4059
5875
|
conflicts.push({
|
|
4060
5876
|
jobIds: [left.jobId, right.jobId].sort(),
|
|
4061
5877
|
key,
|
|
4062
5878
|
kind,
|
|
4063
|
-
...(kind === 'region'
|
|
5879
|
+
...(kind === 'region'
|
|
5880
|
+
? { region: value }
|
|
5881
|
+
: kind === 'symbol'
|
|
5882
|
+
? { symbol: value }
|
|
5883
|
+
: kind === 'public-contract'
|
|
5884
|
+
? { publicContract: value }
|
|
5885
|
+
: { path: value })
|
|
4064
5886
|
});
|
|
4065
5887
|
}
|
|
4066
5888
|
}
|
|
@@ -4077,6 +5899,9 @@ function pairConflictKeys(left, right) {
|
|
|
4077
5899
|
const sharedSymbols = leftKeys.filter((key) => key.startsWith('symbol:') && rightKeySet.has(key));
|
|
4078
5900
|
if (sharedSymbols.length > 0)
|
|
4079
5901
|
return sharedSymbols.sort();
|
|
5902
|
+
const sharedPublicContracts = leftKeys.filter((key) => key.startsWith('public-contract:') && rightKeySet.has(key));
|
|
5903
|
+
if (sharedPublicContracts.length > 0)
|
|
5904
|
+
return sharedPublicContracts.sort();
|
|
4080
5905
|
const leftRegions = leftKeys.filter((key) => key.startsWith('region:'));
|
|
4081
5906
|
const rightRegions = rightKeys.filter((key) => key.startsWith('region:'));
|
|
4082
5907
|
if (leftRegions.length > 0 && rightRegions.length > 0) {
|
|
@@ -4137,6 +5962,11 @@ function stableIdPart(value) {
|
|
|
4137
5962
|
return 'star';
|
|
4138
5963
|
return normalized.length > 0 ? normalized : 'anonymous';
|
|
4139
5964
|
}
|
|
5965
|
+
function publicContractConflictValue(value) {
|
|
5966
|
+
const trimmed = value.trim();
|
|
5967
|
+
const withoutPrefix = trimmed.startsWith('public-contract:') ? trimmed.slice('public-contract:'.length) : trimmed;
|
|
5968
|
+
return stableIdPart(withoutPrefix);
|
|
5969
|
+
}
|
|
4140
5970
|
function reviewerLaneReasons(entry) {
|
|
4141
5971
|
const reasons = [];
|
|
4142
5972
|
if (entry.conflictingJobIds.length)
|
|
@@ -4186,6 +6016,154 @@ function ensureMergeQueueScope(scopes, input) {
|
|
|
4186
6016
|
scopes.set(scope.id, scope);
|
|
4187
6017
|
return scope;
|
|
4188
6018
|
}
|
|
6019
|
+
function createSwarmSemanticLeaseScopeMap(scopes, input = {}) {
|
|
6020
|
+
const out = new Map();
|
|
6021
|
+
for (const scope of scopes) {
|
|
6022
|
+
out.set(scope.id, createSwarmSemanticLeaseScopeForMergeQueueScope(scope, { ...input, scopes }));
|
|
6023
|
+
}
|
|
6024
|
+
return out;
|
|
6025
|
+
}
|
|
6026
|
+
function attachSemanticLeaseScopeToMergeQueueScope(scope, semanticLeaseScopes) {
|
|
6027
|
+
const semanticLeaseScope = semanticLeaseScopes.get(scope.id);
|
|
6028
|
+
return {
|
|
6029
|
+
...scope,
|
|
6030
|
+
...(semanticLeaseScope ? { semanticLeaseScope: cloneSemanticLeaseScope(semanticLeaseScope) } : {})
|
|
6031
|
+
};
|
|
6032
|
+
}
|
|
6033
|
+
function attachSemanticLeaseScopesToAssignment(assignment, semanticLeaseScopes) {
|
|
6034
|
+
const requiredLeaseScopeIds = assignment.requiredLeaseScopeIds?.length ? assignment.requiredLeaseScopeIds : [assignment.scopeId];
|
|
6035
|
+
const requiredSemanticLeaseScopes = requiredLeaseScopeIds
|
|
6036
|
+
.map((scopeId) => semanticLeaseScopes.get(scopeId))
|
|
6037
|
+
.filter((scope) => Boolean(scope))
|
|
6038
|
+
.map(cloneSemanticLeaseScope);
|
|
6039
|
+
const retrySlices = assignment.retrySlices?.map((slice) => {
|
|
6040
|
+
const requiredScopeIds = slice.requiredLeaseScopeIds?.length ? slice.requiredLeaseScopeIds : [slice.scopeId];
|
|
6041
|
+
const requiredScopes = requiredScopeIds
|
|
6042
|
+
.map((scopeId) => semanticLeaseScopes.get(scopeId))
|
|
6043
|
+
.filter((scope) => Boolean(scope))
|
|
6044
|
+
.map(cloneSemanticLeaseScope);
|
|
6045
|
+
const semanticLeaseScope = semanticLeaseScopes.get(slice.scopeId);
|
|
6046
|
+
return {
|
|
6047
|
+
...slice,
|
|
6048
|
+
...(semanticLeaseScope ? { semanticLeaseScope: cloneSemanticLeaseScope(semanticLeaseScope) } : {}),
|
|
6049
|
+
...(requiredScopes.length ? { requiredSemanticLeaseScopes: requiredScopes } : {})
|
|
6050
|
+
};
|
|
6051
|
+
});
|
|
6052
|
+
return {
|
|
6053
|
+
...assignment,
|
|
6054
|
+
...(requiredSemanticLeaseScopes.length ? { requiredSemanticLeaseScopes } : {}),
|
|
6055
|
+
...(retrySlices?.length ? { retrySlices } : {})
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
6058
|
+
function semanticLeaseScopesForCoordinatorAssignment(queue, assignment, input = {}) {
|
|
6059
|
+
if (assignment.requiredSemanticLeaseScopes?.length) {
|
|
6060
|
+
return uniqueSemanticLeaseScopes(assignment.requiredSemanticLeaseScopes.map(cloneSemanticLeaseScope));
|
|
6061
|
+
}
|
|
6062
|
+
const queueScopes = new Map(queue.scopes.map((scope) => [scope.id, scope]));
|
|
6063
|
+
const semanticByKey = new Map(createSwarmSemanticLeaseScopesForMergeQueue(queue, input).map((scope) => [scope.key, scope]));
|
|
6064
|
+
const fromIds = coordinatorAssignmentRequiredLeaseScopeIds(assignment)
|
|
6065
|
+
.map((scopeId) => queueScopes.get(scopeId)?.semanticLeaseScope)
|
|
6066
|
+
.filter((scope) => Boolean(scope));
|
|
6067
|
+
if (fromIds.length)
|
|
6068
|
+
return uniqueSemanticLeaseScopes(fromIds);
|
|
6069
|
+
const fromKeys = coordinatorAssignmentRequiredLeaseKeys(assignment, [])
|
|
6070
|
+
.map((key) => semanticByKey.get(key))
|
|
6071
|
+
.filter((scope) => Boolean(scope));
|
|
6072
|
+
if (fromKeys.length)
|
|
6073
|
+
return uniqueSemanticLeaseScopes(fromKeys);
|
|
6074
|
+
const fallbackKey = 'leaseScope' in assignment ? assignment.leaseScope : assignment.leaseKey;
|
|
6075
|
+
return [defineSemanticLeaseScope({
|
|
6076
|
+
kind: 'custom',
|
|
6077
|
+
key: fallbackKey,
|
|
6078
|
+
repository: input.repository,
|
|
6079
|
+
packageId: input.packageId,
|
|
6080
|
+
metadata: mergeSwarmMetadata([
|
|
6081
|
+
toJsonObject(input.metadata),
|
|
6082
|
+
{
|
|
6083
|
+
swarmQueueId: queue.id,
|
|
6084
|
+
swarmJobId: assignment.jobId,
|
|
6085
|
+
fallbackLeaseKey: fallbackKey
|
|
6086
|
+
}
|
|
6087
|
+
])
|
|
6088
|
+
})];
|
|
6089
|
+
}
|
|
6090
|
+
function coordinatorAssignmentRequiredLeaseScopeIds(assignment) {
|
|
6091
|
+
const explicit = assignment.requiredLeaseScopeIds ?? [];
|
|
6092
|
+
if (explicit.length)
|
|
6093
|
+
return uniqueStrings(explicit);
|
|
6094
|
+
return uniqueStrings(['queueId' in assignment ? assignment.queueId : assignment.scopeId]);
|
|
6095
|
+
}
|
|
6096
|
+
function coordinatorAssignmentRequiredLeaseKeys(assignment, scopes) {
|
|
6097
|
+
const explicit = assignment.requiredLeaseKeys ?? [];
|
|
6098
|
+
if (explicit.length)
|
|
6099
|
+
return uniqueStrings(explicit);
|
|
6100
|
+
if (scopes.length)
|
|
6101
|
+
return uniqueStrings(scopes.map((scope) => scope.key));
|
|
6102
|
+
return uniqueStrings(['leaseScope' in assignment ? assignment.leaseScope : assignment.leaseKey]);
|
|
6103
|
+
}
|
|
6104
|
+
function semanticLeaseScopeOptionsFromMetadata(metadata) {
|
|
6105
|
+
const object = toJsonObject(metadata);
|
|
6106
|
+
if (!object)
|
|
6107
|
+
return {};
|
|
6108
|
+
return {
|
|
6109
|
+
...(typeof object.repository === 'string' ? { repository: object.repository } : {}),
|
|
6110
|
+
...(typeof object.packageId === 'string' ? { packageId: object.packageId } : {})
|
|
6111
|
+
};
|
|
6112
|
+
}
|
|
6113
|
+
function semanticLeaseScopeKindForMergeQueueScope(scope) {
|
|
6114
|
+
if (scope.kind === 'root')
|
|
6115
|
+
return 'repository';
|
|
6116
|
+
if (scope.kind === 'path')
|
|
6117
|
+
return 'path';
|
|
6118
|
+
if (scope.kind === 'lane')
|
|
6119
|
+
return 'lane';
|
|
6120
|
+
if (scope.kind === 'semantic-region' || scope.kind === 'semantic')
|
|
6121
|
+
return semanticLeaseScopeKindForRegion(scope.changedRegions[0]);
|
|
6122
|
+
if (scope.kind === 'package')
|
|
6123
|
+
return 'package';
|
|
6124
|
+
return 'custom';
|
|
6125
|
+
}
|
|
6126
|
+
function semanticLeaseScopeKindForRegion(region) {
|
|
6127
|
+
if (!region)
|
|
6128
|
+
return 'semantic-region';
|
|
6129
|
+
const normalized = region.toLowerCase();
|
|
6130
|
+
if (normalized.includes(FRONTIER_SWARM_SEMANTIC_OWNERSHIP_EXPORT_STABLE_KEY_KIND) || normalized.includes('named-export') || normalized.includes('default-export'))
|
|
6131
|
+
return 'export';
|
|
6132
|
+
if (normalized.includes(FRONTIER_SWARM_SEMANTIC_OWNERSHIP_TYPE_STABLE_KEY_KIND) || normalized.includes('interface') || normalized.includes('type-alias'))
|
|
6133
|
+
return 'type';
|
|
6134
|
+
if (normalized.includes(FRONTIER_SWARM_SEMANTIC_OWNERSHIP_CLI_COMMAND_STABLE_KEY_KIND))
|
|
6135
|
+
return 'cli-command';
|
|
6136
|
+
if (normalized.includes(FRONTIER_SWARM_SEMANTIC_OWNERSHIP_DOCS_SECTION_STABLE_KEY_KIND))
|
|
6137
|
+
return 'docs-section';
|
|
6138
|
+
if (normalized.includes(FRONTIER_SWARM_SEMANTIC_OWNERSHIP_FIXTURE_FAMILY_STABLE_KEY_KIND))
|
|
6139
|
+
return 'test-fixture';
|
|
6140
|
+
if (normalized.includes(FRONTIER_SWARM_SEMANTIC_OWNERSHIP_TEST_CASE_STABLE_KEY_KIND))
|
|
6141
|
+
return 'test-fixture';
|
|
6142
|
+
if (normalized.includes('function'))
|
|
6143
|
+
return 'function';
|
|
6144
|
+
if (normalized.includes('class'))
|
|
6145
|
+
return 'class';
|
|
6146
|
+
if (normalized.includes('member'))
|
|
6147
|
+
return 'member';
|
|
6148
|
+
return 'semantic-region';
|
|
6149
|
+
}
|
|
6150
|
+
function semanticLeaseScopeNameForMergeQueueScope(scope) {
|
|
6151
|
+
const region = scope.changedRegions[0];
|
|
6152
|
+
if (region)
|
|
6153
|
+
return region.split(':').filter(Boolean).at(-1);
|
|
6154
|
+
if (scope.changedPaths.length === 1)
|
|
6155
|
+
return scope.changedPaths[0].split('/').at(-1);
|
|
6156
|
+
return scope.title || scope.id;
|
|
6157
|
+
}
|
|
6158
|
+
function uniqueSemanticLeaseScopes(scopes) {
|
|
6159
|
+
const out = new Map();
|
|
6160
|
+
for (const scope of scopes)
|
|
6161
|
+
out.set(scope.key, cloneSemanticLeaseScope(scope));
|
|
6162
|
+
return Array.from(out.values()).sort((left, right) => left.key.localeCompare(right.key));
|
|
6163
|
+
}
|
|
6164
|
+
function cloneSemanticLeaseScope(scope) {
|
|
6165
|
+
return cloneJsonValue(scope);
|
|
6166
|
+
}
|
|
4189
6167
|
function mergeQueueRootLeaseKey(rootScopeId) {
|
|
4190
6168
|
return `merge:root:${rootScopeId}`;
|
|
4191
6169
|
}
|
|
@@ -4300,6 +6278,8 @@ function cloneMergeQueueRetrySlices(slices) {
|
|
|
4300
6278
|
leaseKey: slice.leaseKey,
|
|
4301
6279
|
...(slice.requiredLeaseScopeIds?.length ? { requiredLeaseScopeIds: [...slice.requiredLeaseScopeIds] } : {}),
|
|
4302
6280
|
...(slice.requiredLeaseKeys?.length ? { requiredLeaseKeys: [...slice.requiredLeaseKeys] } : {}),
|
|
6281
|
+
...(slice.semanticLeaseScope ? { semanticLeaseScope: cloneSemanticLeaseScope(slice.semanticLeaseScope) } : {}),
|
|
6282
|
+
...(slice.requiredSemanticLeaseScopes?.length ? { requiredSemanticLeaseScopes: slice.requiredSemanticLeaseScopes.map(cloneSemanticLeaseScope) } : {}),
|
|
4303
6283
|
...(slice.lane ? { lane: slice.lane } : {}),
|
|
4304
6284
|
changedPaths: [...slice.changedPaths],
|
|
4305
6285
|
changedRegions: [...slice.changedRegions],
|
|
@@ -4349,6 +6329,7 @@ function createHierarchicalQueueLeaseRecords(input) {
|
|
|
4349
6329
|
...(scope.lane ? { lane: scope.lane } : {}),
|
|
4350
6330
|
title: scope.title,
|
|
4351
6331
|
leaseKey: scope.leaseKey,
|
|
6332
|
+
...(scope.semanticLeaseScope ? { semanticLeaseScope: cloneSemanticLeaseScope(scope.semanticLeaseScope) } : {}),
|
|
4352
6333
|
...(hierarchicalQueueLocalLeaderForScope(input, scope) ? { localLeader: hierarchicalQueueLocalLeaderForScope(input, scope) } : {}),
|
|
4353
6334
|
promotion: {
|
|
4354
6335
|
state: promotionState,
|
|
@@ -5186,6 +7167,8 @@ function defaultQueueOutcomeForCategory(category, input, search) {
|
|
|
5186
7167
|
return 'committed';
|
|
5187
7168
|
if (action === 'apply-local' || input.decision === 'applied' || queueOutcomeHas(search, 'applied', 'apply-local', 'apply'))
|
|
5188
7169
|
return 'applied';
|
|
7170
|
+
if (input.decision === 'research-complete' || queueOutcomeHas(search, 'research-complete', 'discovery-complete', 'synthesized'))
|
|
7171
|
+
return 'research-complete';
|
|
5189
7172
|
if (input.decision === 'checked' || queueOutcomeHas(search, 'checked', 'check'))
|
|
5190
7173
|
return 'checked';
|
|
5191
7174
|
if (input.decision === 'superseded' || queueOutcomeHas(search, 'superseded'))
|
|
@@ -5249,6 +7232,8 @@ function canonicalizeSwarmQueueOutcome(value) {
|
|
|
5249
7232
|
return 'superseded';
|
|
5250
7233
|
if (token === 'no-change' || token === 'nochange' || token === 'no-op' || token === 'noop' || token === 'unchanged')
|
|
5251
7234
|
return 'no-change';
|
|
7235
|
+
if (token === 'research-complete' || token === 'discovery-complete' || token === 'synthesized')
|
|
7236
|
+
return 'research-complete';
|
|
5252
7237
|
if (token === 'rejected' || token === 'reject' || token === 'failed' || token === 'failure')
|
|
5253
7238
|
return 'rejected';
|
|
5254
7239
|
if (token === 'rerun' || token === 're-run' || token === 'retry' || token === 'needs-rerun' || token === 'stale-rerun')
|
|
@@ -5263,6 +7248,96 @@ function canonicalizeSwarmQueueOutcome(value) {
|
|
|
5263
7248
|
return 'closed';
|
|
5264
7249
|
return undefined;
|
|
5265
7250
|
}
|
|
7251
|
+
function terminalOutcomeInputForRecord(input) {
|
|
7252
|
+
if (input.outcome && typeof input.outcome === 'object')
|
|
7253
|
+
return input.outcome;
|
|
7254
|
+
return {
|
|
7255
|
+
label: input.label,
|
|
7256
|
+
outcome: typeof input.outcome === 'string' ? input.outcome : undefined,
|
|
7257
|
+
status: input.status,
|
|
7258
|
+
decision: input.decision,
|
|
7259
|
+
reasons: input.reasons,
|
|
7260
|
+
metadata: input.metadata
|
|
7261
|
+
};
|
|
7262
|
+
}
|
|
7263
|
+
function terminalOutcomeRecordStatusFromText(value) {
|
|
7264
|
+
const normalized = normalizeOptionalString(value);
|
|
7265
|
+
if (!normalized)
|
|
7266
|
+
return undefined;
|
|
7267
|
+
const token = normalizeSwarmTerminalOutcomeText(normalized);
|
|
7268
|
+
if (token === 'applied' || token === 'apply-local' || token === 'apply' || token === 'committed' || token === 'commit' || token === 'checked' || token === 'check')
|
|
7269
|
+
return 'applied';
|
|
7270
|
+
if (token === 'rejected' || token === 'reject' || token === 'failed' || token === 'failure')
|
|
7271
|
+
return 'rejected';
|
|
7272
|
+
if (token === 'superseded')
|
|
7273
|
+
return 'superseded';
|
|
7274
|
+
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')
|
|
7275
|
+
return 'no-change';
|
|
7276
|
+
if (token === 'conflict' || token === 'conflict-blocked' || token === 'merge-conflict' || token === 'textual-conflict')
|
|
7277
|
+
return 'conflict';
|
|
7278
|
+
if (token === 'human-needed' || token === 'human-question' || token === 'human-blocked' || token === 'blocked' || token === 'coordinator-review' || token === 'needs-port' || token === 'escalated' || token === 'review')
|
|
7279
|
+
return 'human-needed';
|
|
7280
|
+
if (token === 'research-complete' || token === 'discovery-complete' || token === 'synthesized')
|
|
7281
|
+
return 'research-complete';
|
|
7282
|
+
if (token === 'rerun' || token === 're-run' || token === 'retry' || token === 'needs-rerun' || token === 'stale-rerun')
|
|
7283
|
+
return 'rerun';
|
|
7284
|
+
return token;
|
|
7285
|
+
}
|
|
7286
|
+
function terminalOutcomeRecordStatusFromOutcome(outcome) {
|
|
7287
|
+
return terminalOutcomeRecordStatusFromText(outcome.label)
|
|
7288
|
+
?? terminalOutcomeRecordStatusFromText(outcome.category)
|
|
7289
|
+
?? 'no-change';
|
|
7290
|
+
}
|
|
7291
|
+
function terminalOutcomeRecordStatusIsAdmitted(status) {
|
|
7292
|
+
return status === 'applied'
|
|
7293
|
+
|| status === 'superseded'
|
|
7294
|
+
|| status === 'no-change'
|
|
7295
|
+
|| status === 'research-complete';
|
|
7296
|
+
}
|
|
7297
|
+
function defaultTerminalOutcomeRecordReasonCodes(status) {
|
|
7298
|
+
if (status === 'applied')
|
|
7299
|
+
return ['accepted-by-admission'];
|
|
7300
|
+
if (status === 'rejected')
|
|
7301
|
+
return ['failed-verification'];
|
|
7302
|
+
if (status === 'superseded')
|
|
7303
|
+
return ['superseded-by-newer-output'];
|
|
7304
|
+
if (status === 'no-change')
|
|
7305
|
+
return ['no-effective-change'];
|
|
7306
|
+
if (status === 'conflict')
|
|
7307
|
+
return ['conflict-detected'];
|
|
7308
|
+
if (status === 'human-needed')
|
|
7309
|
+
return ['human-decision-required'];
|
|
7310
|
+
if (status === 'research-complete')
|
|
7311
|
+
return ['research-complete'];
|
|
7312
|
+
if (status === 'rerun')
|
|
7313
|
+
return ['stale-rerun-required'];
|
|
7314
|
+
return [];
|
|
7315
|
+
}
|
|
7316
|
+
function normalizeSwarmTerminalOutcomeReasonCodes(reasonCodes) {
|
|
7317
|
+
return uniqueStrings(uniqueStrings(reasonCodes).map(canonicalizeSwarmTerminalOutcomeReasonCode));
|
|
7318
|
+
}
|
|
7319
|
+
function canonicalizeSwarmTerminalOutcomeReasonCode(value) {
|
|
7320
|
+
const token = normalizeSwarmTerminalOutcomeText(value);
|
|
7321
|
+
if (!token)
|
|
7322
|
+
return value;
|
|
7323
|
+
if (token === 'accepted' || token === 'admitted' || token === 'accepted-by-admission')
|
|
7324
|
+
return 'accepted-by-admission';
|
|
7325
|
+
if (token === 'failed' || token === 'failed-verification' || token === 'verification-failed')
|
|
7326
|
+
return 'failed-verification';
|
|
7327
|
+
if (token === 'superseded' || token === 'superseded-by-newer-output')
|
|
7328
|
+
return 'superseded-by-newer-output';
|
|
7329
|
+
if (token === 'no-change' || token === 'nochange' || token === 'no-effective-change')
|
|
7330
|
+
return 'no-effective-change';
|
|
7331
|
+
if (token === 'conflict' || token === 'conflict-detected' || token === 'merge-conflict')
|
|
7332
|
+
return 'conflict-detected';
|
|
7333
|
+
if (token === 'human-needed' || token === 'human-decision-required' || token === 'human-question')
|
|
7334
|
+
return 'human-decision-required';
|
|
7335
|
+
if (token === 'research-complete' || token === 'discovery-complete')
|
|
7336
|
+
return 'research-complete';
|
|
7337
|
+
if (token === 'rerun' || token === 'stale-rerun-required' || token === 'needs-rerun')
|
|
7338
|
+
return 'stale-rerun-required';
|
|
7339
|
+
return token;
|
|
7340
|
+
}
|
|
5266
7341
|
function queueOutcomeInputsFromMergeQueue(queue) {
|
|
5267
7342
|
return queue.assignments.map((assignment) => ({
|
|
5268
7343
|
subjectAliases: uniqueStrings([assignment.taskId, assignment.jobId, ...assignment.queueItemIds]),
|
|
@@ -5420,6 +7495,7 @@ function queueOutcomeDecisionIsResolvedOutput(decision) {
|
|
|
5420
7495
|
|| decision.outcome === 'checked'
|
|
5421
7496
|
|| decision.outcome === 'superseded'
|
|
5422
7497
|
|| decision.outcome === 'no-change'
|
|
7498
|
+
|| decision.outcome === 'research-complete'
|
|
5423
7499
|
|| decision.outcome === 'recorded'
|
|
5424
7500
|
|| decision.outcome === 'closed';
|
|
5425
7501
|
}
|
|
@@ -5867,6 +7943,253 @@ function countBy(values) {
|
|
|
5867
7943
|
out[value] = (out[value] ?? 0) + 1;
|
|
5868
7944
|
return out;
|
|
5869
7945
|
}
|
|
7946
|
+
const FRONTIER_SWARM_BLOCKING_MERGE_CANDIDATE_REASON_CODES = new Set([
|
|
7947
|
+
'missing-sidecar',
|
|
7948
|
+
'empty-sidecar',
|
|
7949
|
+
'stale-source-hash',
|
|
7950
|
+
'symbol-conflict',
|
|
7951
|
+
'effect-conflict'
|
|
7952
|
+
]);
|
|
7953
|
+
const FRONTIER_SWARM_REVIEW_MERGE_CANDIDATE_REASON_CODES = new Set([
|
|
7954
|
+
'tests-missing'
|
|
7955
|
+
]);
|
|
7956
|
+
const FRONTIER_SWARM_CONFLICT_MERGE_CANDIDATE_REASON_CODES = new Set([
|
|
7957
|
+
'symbol-conflict',
|
|
7958
|
+
'effect-conflict'
|
|
7959
|
+
]);
|
|
7960
|
+
function isSwarmSemanticChange(input) {
|
|
7961
|
+
return input.kind === FRONTIER_SWARM_SEMANTIC_CHANGE_KIND;
|
|
7962
|
+
}
|
|
7963
|
+
function isSwarmMergeCandidate(input) {
|
|
7964
|
+
return input.kind === FRONTIER_SWARM_MERGE_CANDIDATE_KIND;
|
|
7965
|
+
}
|
|
7966
|
+
function normalizeOptionalString(value) {
|
|
7967
|
+
if (typeof value !== 'string')
|
|
7968
|
+
return undefined;
|
|
7969
|
+
const normalized = value.trim();
|
|
7970
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
7971
|
+
}
|
|
7972
|
+
function normalizeSwarmMergeCandidateReasonCodes(reasonCodes) {
|
|
7973
|
+
return uniqueStrings(reasonCodes);
|
|
7974
|
+
}
|
|
7975
|
+
function normalizeSwarmSourceSpan(input, fallback = {}) {
|
|
7976
|
+
const metadata = toJsonObject(input?.metadata);
|
|
7977
|
+
const startLine = readNonNegativeNumber(input?.startLine);
|
|
7978
|
+
const startColumn = readNonNegativeNumber(input?.startColumn);
|
|
7979
|
+
const endLine = readNonNegativeNumber(input?.endLine);
|
|
7980
|
+
const endColumn = readNonNegativeNumber(input?.endColumn);
|
|
7981
|
+
const startOffset = readNonNegativeNumber(input?.startOffset);
|
|
7982
|
+
const endOffset = readNonNegativeNumber(input?.endOffset);
|
|
7983
|
+
const path = normalizeOptionalString(input?.path) ?? normalizeOptionalString(fallback.sourcePath);
|
|
7984
|
+
const sourceHash = normalizeOptionalString(input?.sourceHash) ?? normalizeOptionalString(fallback.sourceHash);
|
|
7985
|
+
const expectedSourceHash = normalizeOptionalString(input?.expectedSourceHash) ?? normalizeOptionalString(fallback.expectedSourceHash);
|
|
7986
|
+
return {
|
|
7987
|
+
...(path ? { path } : {}),
|
|
7988
|
+
...(startLine !== undefined ? { startLine: Math.floor(startLine) } : {}),
|
|
7989
|
+
...(startColumn !== undefined ? { startColumn: Math.floor(startColumn) } : {}),
|
|
7990
|
+
...(endLine !== undefined ? { endLine: Math.floor(endLine) } : {}),
|
|
7991
|
+
...(endColumn !== undefined ? { endColumn: Math.floor(endColumn) } : {}),
|
|
7992
|
+
...(startOffset !== undefined ? { startOffset: Math.floor(startOffset) } : {}),
|
|
7993
|
+
...(endOffset !== undefined ? { endOffset: Math.floor(endOffset) } : {}),
|
|
7994
|
+
...(sourceHash ? { sourceHash } : {}),
|
|
7995
|
+
...(expectedSourceHash ? { expectedSourceHash } : {}),
|
|
7996
|
+
...(metadata ? { metadata } : {})
|
|
7997
|
+
};
|
|
7998
|
+
}
|
|
7999
|
+
function deriveSwarmSemanticChangeReasonCodes(input, sourceSpan) {
|
|
8000
|
+
const reasons = [];
|
|
8001
|
+
if (input.conflictReason)
|
|
8002
|
+
reasons.push(input.conflictReason);
|
|
8003
|
+
if (sourceSpan.sourceHash && sourceSpan.expectedSourceHash && sourceSpan.sourceHash !== sourceSpan.expectedSourceHash) {
|
|
8004
|
+
reasons.push('stale-source-hash');
|
|
8005
|
+
}
|
|
8006
|
+
return normalizeSwarmMergeCandidateReasonCodes(reasons);
|
|
8007
|
+
}
|
|
8008
|
+
function deriveSwarmMergeCandidateReasonCodes(input, semanticChanges, sourceSpan) {
|
|
8009
|
+
const reasons = [];
|
|
8010
|
+
if (input.sidecarRequired && !input.sidecarPath)
|
|
8011
|
+
reasons.push('missing-sidecar');
|
|
8012
|
+
if (input.sidecarEmpty)
|
|
8013
|
+
reasons.push('empty-sidecar');
|
|
8014
|
+
if (input.hasLosses)
|
|
8015
|
+
reasons.push('lossy-import');
|
|
8016
|
+
if (input.testsRequired && input.testsPassed !== true)
|
|
8017
|
+
reasons.push('tests-missing');
|
|
8018
|
+
if (input.conflictReason)
|
|
8019
|
+
reasons.push(input.conflictReason);
|
|
8020
|
+
if (sourceSpan.sourceHash && sourceSpan.expectedSourceHash && sourceSpan.sourceHash !== sourceSpan.expectedSourceHash) {
|
|
8021
|
+
reasons.push('stale-source-hash');
|
|
8022
|
+
}
|
|
8023
|
+
if (semanticChanges.length === 0 && input.sidecarPath)
|
|
8024
|
+
reasons.push('empty-sidecar');
|
|
8025
|
+
return normalizeSwarmMergeCandidateReasonCodes(reasons);
|
|
8026
|
+
}
|
|
8027
|
+
function normalizeSwarmSemanticConfidence(value) {
|
|
8028
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
8029
|
+
return clamp01(value);
|
|
8030
|
+
const normalized = normalizeOptionalString(value);
|
|
8031
|
+
return normalized ?? 'medium';
|
|
8032
|
+
}
|
|
8033
|
+
function normalizeSwarmConflictReason(conflictReason, reasonCodes) {
|
|
8034
|
+
const explicit = normalizeOptionalString(conflictReason);
|
|
8035
|
+
if (explicit)
|
|
8036
|
+
return explicit;
|
|
8037
|
+
return reasonCodes.find((reason) => FRONTIER_SWARM_CONFLICT_MERGE_CANDIDATE_REASON_CODES.has(reason));
|
|
8038
|
+
}
|
|
8039
|
+
function normalizeGraphNode(input, generatedAt = Date.now()) {
|
|
8040
|
+
const metadata = toJsonObject(input.metadata);
|
|
8041
|
+
const id = input.id ?? graphNodeId(input.kind, input.title ?? input.jobId ?? input.taskId ?? String(generatedAt));
|
|
8042
|
+
return {
|
|
8043
|
+
id,
|
|
8044
|
+
kind: input.kind,
|
|
8045
|
+
...(input.title ? { title: input.title } : {}),
|
|
8046
|
+
...(input.status ? { status: input.status } : {}),
|
|
8047
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
8048
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
8049
|
+
...(input.path ? { path: input.path } : {}),
|
|
8050
|
+
generatedAt: input.generatedAt ?? generatedAt,
|
|
8051
|
+
...(metadata ? { metadata } : {})
|
|
8052
|
+
};
|
|
8053
|
+
}
|
|
8054
|
+
function normalizeGraphEdge(input, generatedAt = Date.now()) {
|
|
8055
|
+
const metadata = toJsonObject(input.metadata);
|
|
8056
|
+
return {
|
|
8057
|
+
id: input.id ?? `${input.kind}:${input.from}->${input.to}`,
|
|
8058
|
+
from: input.from,
|
|
8059
|
+
to: input.to,
|
|
8060
|
+
kind: input.kind,
|
|
8061
|
+
...(input.label ? { label: input.label } : {}),
|
|
8062
|
+
generatedAt: input.generatedAt ?? generatedAt,
|
|
8063
|
+
...(metadata ? { metadata } : {})
|
|
8064
|
+
};
|
|
8065
|
+
}
|
|
8066
|
+
function normalizeGraphEvent(input, generatedAt = Date.now()) {
|
|
8067
|
+
const timestamp = input.timestamp ?? generatedAt;
|
|
8068
|
+
const metadata = toJsonObject(input.metadata);
|
|
8069
|
+
return {
|
|
8070
|
+
id: input.id ?? 'swarm-graph-event:' + stableHash([input.type, input.nodeId, input.edgeId, timestamp]),
|
|
8071
|
+
type: input.type,
|
|
8072
|
+
...(input.nodeId ? { nodeId: input.nodeId } : {}),
|
|
8073
|
+
...(input.edgeId ? { edgeId: input.edgeId } : {}),
|
|
8074
|
+
timestamp,
|
|
8075
|
+
...(metadata ? { metadata } : {})
|
|
8076
|
+
};
|
|
8077
|
+
}
|
|
8078
|
+
function isPassingGraphStatus(status) {
|
|
8079
|
+
return status === 'passed' || status === 'accepted' || status === 'verified' || status === 'completed';
|
|
8080
|
+
}
|
|
8081
|
+
function isBlockingGraphStatus(status) {
|
|
8082
|
+
return status === 'failed' || status === 'rejected' || status === 'blocked';
|
|
8083
|
+
}
|
|
8084
|
+
function dedupeGraphNodes(nodes) {
|
|
8085
|
+
const byId = new Map();
|
|
8086
|
+
for (const node of nodes)
|
|
8087
|
+
byId.set(node.id, mergeGraphNode(byId.get(node.id), node));
|
|
8088
|
+
return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
8089
|
+
}
|
|
8090
|
+
function mergeGraphNode(existing, node) {
|
|
8091
|
+
if (!existing)
|
|
8092
|
+
return node;
|
|
8093
|
+
return {
|
|
8094
|
+
...existing,
|
|
8095
|
+
...node,
|
|
8096
|
+
metadata: mergeGraphMetadata(existing.metadata, node.metadata)
|
|
8097
|
+
};
|
|
8098
|
+
}
|
|
8099
|
+
function dedupeGraphEdges(edges) {
|
|
8100
|
+
const byId = new Map();
|
|
8101
|
+
for (const edge of edges)
|
|
8102
|
+
byId.set(edge.id, edge);
|
|
8103
|
+
return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
8104
|
+
}
|
|
8105
|
+
function summarizeSwarmRunGraph(nodes, edges, events) {
|
|
8106
|
+
return {
|
|
8107
|
+
nodeCount: nodes.length,
|
|
8108
|
+
edgeCount: edges.length,
|
|
8109
|
+
eventCount: events.length,
|
|
8110
|
+
nodeKinds: countBy(nodes.map((node) => node.kind)),
|
|
8111
|
+
edgeKinds: countBy(edges.map((edge) => edge.kind)),
|
|
8112
|
+
openBlockerCount: nodes.filter((node) => node.status === 'blocked').length + edges.filter((edge) => edge.kind === 'blocks').length,
|
|
8113
|
+
humanQuestionCount: nodes.filter((node) => node.kind === 'decision' && /human|question/i.test(node.title ?? '')).length
|
|
8114
|
+
};
|
|
8115
|
+
}
|
|
8116
|
+
function graphNodeId(kind, value) {
|
|
8117
|
+
return `${kind}:${normalizeGraphIdPart(value)}`;
|
|
8118
|
+
}
|
|
8119
|
+
function normalizeGraphIdPart(value) {
|
|
8120
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '') || stableHash(value);
|
|
8121
|
+
}
|
|
8122
|
+
function normalizeSwarmGraphRef(input) {
|
|
8123
|
+
return {
|
|
8124
|
+
id: input.id ?? 'swarm-graph-ref:' + stableHash([input.kind, input.graphId, input.nodeId, input.edgeId, input.role]),
|
|
8125
|
+
kind: input.kind ?? 'graph',
|
|
8126
|
+
...(input.graphId ? { graphId: input.graphId } : {}),
|
|
8127
|
+
...(input.nodeId ? { nodeId: input.nodeId } : {}),
|
|
8128
|
+
...(input.edgeId ? { edgeId: input.edgeId } : {}),
|
|
8129
|
+
...(input.role ? { role: input.role } : {}),
|
|
8130
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
8131
|
+
};
|
|
8132
|
+
}
|
|
8133
|
+
function normalizeSwarmGraphRefs(input) {
|
|
8134
|
+
return input.map(normalizeSwarmGraphRef).sort((left, right) => left.id.localeCompare(right.id));
|
|
8135
|
+
}
|
|
8136
|
+
function normalizeSwarmPatchEvent(input, replayRecordId, index) {
|
|
8137
|
+
if (isSwarmPatchEvent(input) && (!replayRecordId || input.replayRecordId) && (index === undefined || input.sequence !== undefined)) {
|
|
8138
|
+
return input;
|
|
8139
|
+
}
|
|
8140
|
+
return createSwarmPatchEvent({
|
|
8141
|
+
...input,
|
|
8142
|
+
replayRecordId: input.replayRecordId ?? replayRecordId,
|
|
8143
|
+
sequence: input.sequence ?? index
|
|
8144
|
+
});
|
|
8145
|
+
}
|
|
8146
|
+
function inferImprovementLoopStatus(input) {
|
|
8147
|
+
if (input.result !== undefined)
|
|
8148
|
+
return 'applied';
|
|
8149
|
+
if (input.action !== undefined)
|
|
8150
|
+
return 'planned';
|
|
8151
|
+
if (input.observation !== undefined)
|
|
8152
|
+
return 'observed';
|
|
8153
|
+
return 'unknown';
|
|
8154
|
+
}
|
|
8155
|
+
function createImprovementLoopPhaseNode(loop, phase, value, generatedAt) {
|
|
8156
|
+
return {
|
|
8157
|
+
id: graphNodeId('rsi', `${loop.id}:${phase}`),
|
|
8158
|
+
kind: 'rsi',
|
|
8159
|
+
title: `${loop.title} ${phase}`,
|
|
8160
|
+
status: loop.status,
|
|
8161
|
+
jobId: loop.jobId,
|
|
8162
|
+
taskId: loop.taskId,
|
|
8163
|
+
generatedAt,
|
|
8164
|
+
metadata: {
|
|
8165
|
+
phase,
|
|
8166
|
+
value,
|
|
8167
|
+
loop
|
|
8168
|
+
}
|
|
8169
|
+
};
|
|
8170
|
+
}
|
|
8171
|
+
function isSwarmGateRecord(input) {
|
|
8172
|
+
return input.kind === FRONTIER_SWARM_GATE_RECORD_KIND;
|
|
8173
|
+
}
|
|
8174
|
+
function isSwarmEvidenceRecord(input) {
|
|
8175
|
+
return input.kind === FRONTIER_SWARM_EVIDENCE_RECORD_KIND;
|
|
8176
|
+
}
|
|
8177
|
+
function isSwarmPatchEvent(input) {
|
|
8178
|
+
return input.kind === FRONTIER_SWARM_PATCH_EVENT_KIND;
|
|
8179
|
+
}
|
|
8180
|
+
function isSwarmReplayRecord(input) {
|
|
8181
|
+
return input.kind === FRONTIER_SWARM_REPLAY_RECORD_KIND;
|
|
8182
|
+
}
|
|
8183
|
+
function isSwarmImprovementLoop(input) {
|
|
8184
|
+
return input.kind === FRONTIER_SWARM_IMPROVEMENT_LOOP_KIND;
|
|
8185
|
+
}
|
|
8186
|
+
function mergeGraphMetadata(left, right) {
|
|
8187
|
+
if (!left)
|
|
8188
|
+
return right;
|
|
8189
|
+
if (!right)
|
|
8190
|
+
return left;
|
|
8191
|
+
return { ...left, ...right };
|
|
8192
|
+
}
|
|
5870
8193
|
function hasJobDependencyCycle(start, dependenciesByJobId) {
|
|
5871
8194
|
const visiting = new Set();
|
|
5872
8195
|
const visited = new Set();
|
|
@@ -8123,6 +10446,141 @@ function joinPathParts(...parts) {
|
|
|
8123
10446
|
.filter(Boolean)
|
|
8124
10447
|
.join('/');
|
|
8125
10448
|
}
|
|
10449
|
+
function swarmRunEventTime(options, fallback) {
|
|
10450
|
+
if (options.time)
|
|
10451
|
+
return options.time;
|
|
10452
|
+
const value = options.now ?? fallback;
|
|
10453
|
+
return new Date(Number.isFinite(value) ? value : Date.now()).toISOString();
|
|
10454
|
+
}
|
|
10455
|
+
function isFrontierRunEventList(input) {
|
|
10456
|
+
return Array.isArray(input);
|
|
10457
|
+
}
|
|
10458
|
+
function swarmRunLaneNodeId(lane) {
|
|
10459
|
+
return String(lane).startsWith('lane:') ? String(lane) : `lane:${lane}`;
|
|
10460
|
+
}
|
|
10461
|
+
function swarmRunTaskNodeId(taskId) {
|
|
10462
|
+
return String(taskId).startsWith('task:') ? String(taskId) : `task:${taskId}`;
|
|
10463
|
+
}
|
|
10464
|
+
function swarmRunAttemptNodeId(jobId) {
|
|
10465
|
+
return String(jobId).startsWith('attempt:') ? String(jobId) : `attempt:${jobId}`;
|
|
10466
|
+
}
|
|
10467
|
+
function swarmRunLeaseNodeId(leaseId) {
|
|
10468
|
+
return String(leaseId).startsWith('lease:') ? String(leaseId) : `lease:${leaseId}`;
|
|
10469
|
+
}
|
|
10470
|
+
function swarmRunPatchNodeId(jobId) {
|
|
10471
|
+
return String(jobId).startsWith('patch:') ? String(jobId) : `patch:${jobId}`;
|
|
10472
|
+
}
|
|
10473
|
+
function swarmRunArtifactNodeId(artifactPath) {
|
|
10474
|
+
return `artifact:${slug(artifactPath)}:${stableHash(artifactPath).slice('fnv1a32:'.length)}`;
|
|
10475
|
+
}
|
|
10476
|
+
function swarmRunEvidenceNodeId(jobId, evidencePath) {
|
|
10477
|
+
return `evidence:${slug(jobId)}:${stableHash(evidencePath).slice('fnv1a32:'.length)}`;
|
|
10478
|
+
}
|
|
10479
|
+
function swarmRunVerificationNodeId(jobId, index, name) {
|
|
10480
|
+
return `verification:${slug(jobId)}:${index}:${slug(name || 'command')}`;
|
|
10481
|
+
}
|
|
10482
|
+
function swarmRunDecisionNodeId(id) {
|
|
10483
|
+
return String(id).startsWith('decision:') ? String(id) : `decision:${id}`;
|
|
10484
|
+
}
|
|
10485
|
+
function inferSwarmLanePackageId(jobs) {
|
|
10486
|
+
for (const job of jobs) {
|
|
10487
|
+
const candidate = job.task.metadata?.packageId ?? job.metadata?.packageId ?? job.task.metadata?.package ?? job.metadata?.package;
|
|
10488
|
+
if (typeof candidate === 'string' && candidate)
|
|
10489
|
+
return candidate;
|
|
10490
|
+
}
|
|
10491
|
+
return undefined;
|
|
10492
|
+
}
|
|
10493
|
+
function formatSwarmCommandLine(command) {
|
|
10494
|
+
return [command.command, ...command.args].join(' ');
|
|
10495
|
+
}
|
|
10496
|
+
function swarmStatusToRunStatus(status) {
|
|
10497
|
+
if (status === 'completed' || status === 'verified')
|
|
10498
|
+
return 'done';
|
|
10499
|
+
if (status === 'failed' || status === 'rejected')
|
|
10500
|
+
return 'rejected';
|
|
10501
|
+
if (status === 'blocked')
|
|
10502
|
+
return 'blocked';
|
|
10503
|
+
if (status === 'running')
|
|
10504
|
+
return 'running';
|
|
10505
|
+
if (status === 'planned')
|
|
10506
|
+
return 'planned';
|
|
10507
|
+
return 'ready';
|
|
10508
|
+
}
|
|
10509
|
+
function swarmResultStatusToAttemptStatus(status) {
|
|
10510
|
+
if (status === 'completed' || status === 'verified')
|
|
10511
|
+
return 'completed';
|
|
10512
|
+
if (status === 'running')
|
|
10513
|
+
return 'running';
|
|
10514
|
+
if (status === 'cancelled')
|
|
10515
|
+
return 'cancelled';
|
|
10516
|
+
if (status === 'timed-out' || status === 'timeout')
|
|
10517
|
+
return 'timed-out';
|
|
10518
|
+
if (status === 'queued' || status === 'ready' || status === 'planned')
|
|
10519
|
+
return 'queued';
|
|
10520
|
+
return 'failed';
|
|
10521
|
+
}
|
|
10522
|
+
function swarmRiskToRunRisk(risk) {
|
|
10523
|
+
if (risk === 'low' || risk === 'medium' || risk === 'high')
|
|
10524
|
+
return risk;
|
|
10525
|
+
return 'medium';
|
|
10526
|
+
}
|
|
10527
|
+
function inferSwarmArtifactType(file) {
|
|
10528
|
+
const lower = file.toLowerCase();
|
|
10529
|
+
if (lower.endsWith('.patch') || lower.endsWith('.diff'))
|
|
10530
|
+
return 'patch';
|
|
10531
|
+
if (lower.endsWith('.json') || lower.endsWith('.jsonl'))
|
|
10532
|
+
return 'json';
|
|
10533
|
+
if (lower.endsWith('.log') || lower.endsWith('.txt') || lower.includes('stdout') || lower.includes('stderr'))
|
|
10534
|
+
return 'log';
|
|
10535
|
+
if (lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg') || lower.endsWith('.webp'))
|
|
10536
|
+
return 'image';
|
|
10537
|
+
return 'artifact';
|
|
10538
|
+
}
|
|
10539
|
+
function swarmDispositionToRunDecision(disposition, readiness) {
|
|
10540
|
+
if (disposition === 'auto-mergeable' || readiness === 'verified-patch')
|
|
10541
|
+
return 'apply';
|
|
10542
|
+
if (disposition === 'stale-against-head')
|
|
10543
|
+
return 'rerun';
|
|
10544
|
+
if (disposition === 'rejected' || readiness === 'rejected')
|
|
10545
|
+
return 'reject';
|
|
10546
|
+
if (disposition === 'needs-port' || disposition === 'blocked' || readiness === 'blocked')
|
|
10547
|
+
return 'human-question';
|
|
10548
|
+
return 'record-only';
|
|
10549
|
+
}
|
|
10550
|
+
function swarmQueueOutcomeToRunDecision(decision) {
|
|
10551
|
+
if (decision.outcome === 'applied' || decision.decision === 'applied' || decision.action === 'apply-local')
|
|
10552
|
+
return 'apply';
|
|
10553
|
+
if (decision.outcome === 'rerun' || decision.decision === 'rerun' || decision.staleOrRerun)
|
|
10554
|
+
return 'rerun';
|
|
10555
|
+
if (decision.outcome === 'rejected' || decision.decision === 'rejected')
|
|
10556
|
+
return 'reject';
|
|
10557
|
+
if (decision.action === 'promote')
|
|
10558
|
+
return 'promote';
|
|
10559
|
+
if (decision.humanBlocked || decision.outcome === 'human-blocked')
|
|
10560
|
+
return 'human-question';
|
|
10561
|
+
return 'record-only';
|
|
10562
|
+
}
|
|
10563
|
+
function swarmQueueOutcomeRequiredActions(decision) {
|
|
10564
|
+
const actions = [];
|
|
10565
|
+
if (decision.coordinatorReview || decision.reviewDebt)
|
|
10566
|
+
actions.push('coordinator-review');
|
|
10567
|
+
if (decision.humanBlocked)
|
|
10568
|
+
actions.push('human-answer');
|
|
10569
|
+
if (decision.staleOrRerun)
|
|
10570
|
+
actions.push('rerun');
|
|
10571
|
+
if (decision.conflict)
|
|
10572
|
+
actions.push('resolve-conflict');
|
|
10573
|
+
return uniqueStrings(actions);
|
|
10574
|
+
}
|
|
10575
|
+
function pruneUndefinedJsonObject(value) {
|
|
10576
|
+
const out = {};
|
|
10577
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
10578
|
+
if (entry === undefined)
|
|
10579
|
+
continue;
|
|
10580
|
+
out[key] = toJsonValue(entry);
|
|
10581
|
+
}
|
|
10582
|
+
return out;
|
|
10583
|
+
}
|
|
8126
10584
|
function normalizeId(value, label) {
|
|
8127
10585
|
const id = String(value || '').trim();
|
|
8128
10586
|
if (!id)
|