@shapeshift-labs/frontier-loom-ui 0.1.2 → 0.1.4
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 +8 -8
- package/dist/cli.js +1 -6
- package/dist/cli.js.map +1 -1
- package/dist/client.js +710 -13
- package/dist/client.js.map +1 -1
- package/dist/public/styles.css +168 -0
- package/dist/server.js +1701 -84
- package/dist/server.js.map +1 -1
- package/features/loom-dashboard-substrate-integration.json +58 -0
- package/package.json +6 -2
package/dist/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "@shapeshift-labs/frontier-dom/jsx-ru
|
|
|
2
2
|
const contentTabs = [
|
|
3
3
|
{ id: 'work', label: 'Overview' },
|
|
4
4
|
{ id: 'board', label: 'Board' },
|
|
5
|
+
{ id: 'running', label: 'Running' },
|
|
5
6
|
{ id: 'swarm', label: 'Swarm' },
|
|
6
7
|
{ id: 'lanes', label: 'Lanes' },
|
|
7
8
|
{ id: 'performance', label: 'Performance' },
|
|
@@ -18,6 +19,7 @@ let renderedDashboardSignature;
|
|
|
18
19
|
let pendingFocusTab;
|
|
19
20
|
let selectedTaskCardId = initialRoute.taskId;
|
|
20
21
|
let selectedTicketId = initialRoute.ticket;
|
|
22
|
+
let runningGraphFilter = { lane: 'all', task: 'all' };
|
|
21
23
|
let chartPopover;
|
|
22
24
|
let activeContributionTarget;
|
|
23
25
|
const taskDetailsCache = new Map();
|
|
@@ -56,6 +58,24 @@ root?.addEventListener('click', (event) => {
|
|
|
56
58
|
closeTaskDialog();
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
61
|
+
const runningFilter = event.target instanceof Element
|
|
62
|
+
? event.target.closest('[data-running-filter-kind]')
|
|
63
|
+
: null;
|
|
64
|
+
if (runningFilter) {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
const kind = runningFilter.dataset.runningFilterKind;
|
|
68
|
+
const value = runningFilter.dataset.runningFilterValue || 'all';
|
|
69
|
+
if (kind === 'lane') {
|
|
70
|
+
runningGraphFilter = { lane: value, task: 'all' };
|
|
71
|
+
}
|
|
72
|
+
else if (kind === 'task') {
|
|
73
|
+
runningGraphFilter = { ...runningGraphFilter, task: value };
|
|
74
|
+
}
|
|
75
|
+
if (currentDashboard)
|
|
76
|
+
renderDashboard(currentDashboard);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
59
79
|
const tab = event.target instanceof Element
|
|
60
80
|
? event.target.closest('[data-content-tab]')
|
|
61
81
|
: null;
|
|
@@ -272,6 +292,7 @@ function dashboardSignature(dashboard) {
|
|
|
272
292
|
events: dashboard.events,
|
|
273
293
|
routing: dashboard.routing,
|
|
274
294
|
backlog: dashboard.backlog,
|
|
295
|
+
capacity: dashboard.capacity,
|
|
275
296
|
semantic: dashboard.semantic,
|
|
276
297
|
sources: dashboard.sources
|
|
277
298
|
});
|
|
@@ -309,6 +330,10 @@ function contentPanel(tab, input) {
|
|
|
309
330
|
return _jsx(Panel, { title: "Overview", meta: `${text(input.visibleJobs.length)} tasks`, hideHead: true, children: _jsx(WorkOverview, { dashboard: input.dashboard, lanes: input.lanes, jobs: input.visibleJobs, attention: input.attention, audit: input.audit, success: input.success }) });
|
|
310
331
|
if (tab === 'board')
|
|
311
332
|
return _jsx(Panel, { title: "Task board", meta: boardPanelMeta(input.dashboard, boardItems, input.visibleJobs), children: _jsx(TaskBoard, { dashboard: input.dashboard, jobs: input.visibleJobs }) });
|
|
333
|
+
if (tab === 'running') {
|
|
334
|
+
const activeCount = activeAgentTaskCount(input.dashboard, input.visibleJobs);
|
|
335
|
+
return _jsx(Panel, { title: "Running graph", meta: activeCount ? `${text(activeCount)} active tasks` : 'none running', children: _jsx(RunningGraphView, { dashboard: input.dashboard, jobs: input.visibleJobs }) });
|
|
336
|
+
}
|
|
312
337
|
if (tab === 'swarm') {
|
|
313
338
|
const sourceKind = dashboardSourceKind(input.dashboard);
|
|
314
339
|
const activeCount = activeAgentTaskCount(input.dashboard, input.visibleJobs);
|
|
@@ -424,6 +449,204 @@ function TaskBoard({ dashboard, jobs }) {
|
|
|
424
449
|
? column.items.map((job) => _jsx(TaskBoardCard, { job: job }))
|
|
425
450
|
: _jsx("p", { className: "empty tight", children: column.empty }) })] })) }) });
|
|
426
451
|
}
|
|
452
|
+
function RunningGraphView({ dashboard, jobs }) {
|
|
453
|
+
const graph = buildRunningGraph(dashboard, jobs, runningGraphFilter);
|
|
454
|
+
return _jsxs("div", { className: "running-layout", "data-scroll-id": "running", "data-smoke-marker": "running-tab", children: [_jsxs("section", { className: "running-filters", "aria-label": "Running graph filters", children: [_jsxs("div", { className: "running-filter-group", children: [_jsx("span", { children: "Scope" }), _jsxs("div", { children: [_jsx(RunningFilterButton, { kind: "lane", value: "all", active: graph.filter.lane === 'all', label: `Global · ${text(graph.totalActiveJobCount)}` }), graph.laneOptions.map((lane) => _jsx(RunningFilterButton, { kind: "lane", value: lane.id, active: graph.filter.lane === lane.id, label: `${lane.label} · ${text(lane.count)}` }))] })] }), _jsxs("div", { className: "running-filter-group", children: [_jsx("span", { children: "Tasks" }), _jsxs("div", { children: [_jsx(RunningFilterButton, { kind: "task", value: "all", active: graph.filter.task === 'all', label: "All active" }), graph.taskOptions.map((task) => _jsx(RunningFilterButton, { kind: "task", value: task.id, active: graph.filter.task === task.id, label: `${task.ticket} · ${task.label}` }))] })] })] }), _jsxs("section", { className: "work-section running-graph-section", children: [_jsxs("div", { className: "metric-section-head", children: [_jsx("h3", { children: "Live execution graph" }), _jsxs("span", { children: [text(graph.activeAgentCount), " agents \u00B7 ", text(graph.activeJobCount), " active tasks"] })] }), _jsx(RunningGraphCanvas, { graph: graph })] })] });
|
|
455
|
+
}
|
|
456
|
+
function RunningFilterButton({ kind, value, active, label }) {
|
|
457
|
+
return _jsx("button", { type: "button", className: active ? 'running-filter active' : 'running-filter', "data-running-filter-kind": kind, "data-running-filter-value": value, children: label });
|
|
458
|
+
}
|
|
459
|
+
function RunningGraphCanvas({ graph }) {
|
|
460
|
+
if (!graph.activeJobCount)
|
|
461
|
+
return _jsx("p", { className: "empty tight", children: "No active jobs are running right now." });
|
|
462
|
+
return _jsx("div", { className: "running-graph-viewport", "data-scroll-id": "running-graph-viewport", "aria-label": "Current running swarm graph", children: _jsxs("div", { className: "running-graph-canvas", style: `width:${graph.width}px; height:${graph.height}px`, children: [_jsx("svg", { className: "running-graph-svg", viewBox: `0 0 ${graph.width} ${graph.height}`, width: graph.width, height: graph.height, "aria-hidden": "true", children: graph.edges.map((edge) => _jsx("path", { className: `running-edge ${edge.tone}`, d: edge.path })) }), graph.nodes.map((node) => _jsx(RunningGraphNodeView, { node: node }))] }) });
|
|
463
|
+
}
|
|
464
|
+
function RunningGraphNodeView({ node }) {
|
|
465
|
+
const style = `left:${node.x}px; top:${node.y}px; width:${node.width}px; height:${node.height}px; --node-color:${node.color ?? 'var(--line-strong)'}`;
|
|
466
|
+
if (node.taskCardId)
|
|
467
|
+
return _jsx("button", { type: "button", className: `running-node ${node.kind} ${node.tone}`, style: style, "data-task-card": node.taskCardId, "aria-label": `${node.label}: ${node.detail}. ${node.meta}`, children: _jsx(RunningGraphNodeContent, { node: node }) });
|
|
468
|
+
return _jsx("article", { className: `running-node ${node.kind} ${node.tone}`, style: style, "aria-label": `${node.label}: ${node.detail}. ${node.meta}`, children: _jsx(RunningGraphNodeContent, { node: node }) });
|
|
469
|
+
}
|
|
470
|
+
function RunningGraphNodeContent({ node }) {
|
|
471
|
+
return _jsxs("div", { className: "running-node-inner", children: [_jsx("span", { children: node.kind }), _jsx("b", { children: node.label }), _jsx("small", { children: node.detail }), _jsx("em", { children: node.meta })] });
|
|
472
|
+
}
|
|
473
|
+
function buildRunningGraph(dashboard, jobs, filter) {
|
|
474
|
+
const now = timeValue(dashboard.generatedAt) ?? Date.now();
|
|
475
|
+
const activeItems = taskBoardItems(dashboard, jobs).filter(isActiveAgentJob);
|
|
476
|
+
const laneCounts = runningLaneCounts(activeItems);
|
|
477
|
+
const laneOptions = Array.from(laneCounts.entries())
|
|
478
|
+
.map(([id, count]) => ({ id, label: laneDisplayLabel(id), count }))
|
|
479
|
+
.sort((left, right) => right.count - left.count || left.label.localeCompare(right.label));
|
|
480
|
+
const laneFilter = filter.lane !== 'all' && laneCounts.has(filter.lane) ? filter.lane : 'all';
|
|
481
|
+
const laneScopedItems = laneFilter === 'all'
|
|
482
|
+
? activeItems
|
|
483
|
+
: activeItems.filter((job) => laneOf(job) === laneFilter);
|
|
484
|
+
const taskOptions = laneScopedItems
|
|
485
|
+
.map((job) => ({ id: taskCardId(job), label: taskTitle(job), ticket: ticketId(job), lane: laneOf(job) }))
|
|
486
|
+
.sort((left, right) => left.ticket.localeCompare(right.ticket));
|
|
487
|
+
const taskIds = new Set(taskOptions.map((task) => task.id));
|
|
488
|
+
const taskFilter = filter.task !== 'all' && taskIds.has(filter.task) ? filter.task : 'all';
|
|
489
|
+
const scopedItems = taskFilter === 'all'
|
|
490
|
+
? laneScopedItems
|
|
491
|
+
: laneScopedItems.filter((job) => taskCardId(job) === taskFilter);
|
|
492
|
+
const dimensions = {
|
|
493
|
+
source: { x: 28, width: 196, height: 92 },
|
|
494
|
+
lane: { x: 276, width: 220, height: 92 },
|
|
495
|
+
agent: { x: 560, width: 286, height: 104 },
|
|
496
|
+
task: { x: 918, width: 430, height: 112 },
|
|
497
|
+
rowHeight: 148,
|
|
498
|
+
paddingY: 38,
|
|
499
|
+
groupGap: 40
|
|
500
|
+
};
|
|
501
|
+
const nodes = [];
|
|
502
|
+
const laneNodes = [];
|
|
503
|
+
const agentNodes = [];
|
|
504
|
+
const taskNodes = [];
|
|
505
|
+
const edges = [];
|
|
506
|
+
let yCursor = dimensions.paddingY;
|
|
507
|
+
const laneIds = uniqueStrings(scopedItems.map(laneOf)).sort((left, right) => laneDisplayLabel(left).localeCompare(laneDisplayLabel(right)));
|
|
508
|
+
for (const laneId of laneIds) {
|
|
509
|
+
const laneJobs = scopedItems.filter((job) => laneOf(job) === laneId);
|
|
510
|
+
const workers = agentWorkers(laneJobs);
|
|
511
|
+
const laneStartY = yCursor;
|
|
512
|
+
let laneRows = 0;
|
|
513
|
+
workers.forEach((worker, workerIndex) => {
|
|
514
|
+
const workerJobs = worker.currentJobs.length ? worker.currentJobs : worker.jobs;
|
|
515
|
+
const workerStartRow = laneRows;
|
|
516
|
+
workerJobs.forEach((job) => {
|
|
517
|
+
const y = laneStartY + laneRows * dimensions.rowHeight;
|
|
518
|
+
taskNodes.push({
|
|
519
|
+
id: `task:${taskCardId(job)}`,
|
|
520
|
+
kind: 'task',
|
|
521
|
+
label: taskTitle(job),
|
|
522
|
+
detail: `${ticketId(job)} · ${taskCardStatus(job)}`,
|
|
523
|
+
meta: `${agentModelLabel(job)} · ${jobRuntimeLabel(job, now)}`,
|
|
524
|
+
x: dimensions.task.x,
|
|
525
|
+
y,
|
|
526
|
+
width: dimensions.task.width,
|
|
527
|
+
height: dimensions.task.height,
|
|
528
|
+
tone: 'good',
|
|
529
|
+
taskCardId: taskCardId(job)
|
|
530
|
+
});
|
|
531
|
+
laneRows += 1;
|
|
532
|
+
});
|
|
533
|
+
const workerRowCount = Math.max(1, workerJobs.length);
|
|
534
|
+
const workerY = laneStartY
|
|
535
|
+
+ workerStartRow * dimensions.rowHeight
|
|
536
|
+
+ ((workerRowCount - 1) * dimensions.rowHeight) / 2;
|
|
537
|
+
agentNodes.push({
|
|
538
|
+
id: `agent:${laneId}:${worker.key}`,
|
|
539
|
+
kind: 'agent',
|
|
540
|
+
label: agentDisplayName(worker, workerIndex),
|
|
541
|
+
detail: `${agentModelSummary(worker)} · ${agentRuntimeValue(worker, now)}`,
|
|
542
|
+
meta: `${text(workerJobs.length)} ${workerJobs.length === 1 ? 'task' : 'tasks'} · ${agentTokenValue(worker)} input`,
|
|
543
|
+
x: dimensions.agent.x,
|
|
544
|
+
y: workerY,
|
|
545
|
+
width: dimensions.agent.width,
|
|
546
|
+
height: dimensions.agent.height,
|
|
547
|
+
tone: 'neutral',
|
|
548
|
+
color: worker.color
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
const safeLaneRows = Math.max(1, laneRows);
|
|
552
|
+
const laneY = laneStartY + ((safeLaneRows - 1) * dimensions.rowHeight) / 2;
|
|
553
|
+
laneNodes.push({
|
|
554
|
+
id: `lane:${laneId}`,
|
|
555
|
+
kind: 'lane',
|
|
556
|
+
label: laneDisplayLabel(laneId),
|
|
557
|
+
detail: `${text(laneJobs.length)} active ${laneJobs.length === 1 ? 'task' : 'tasks'}`,
|
|
558
|
+
meta: laneModelSummary(laneJobs),
|
|
559
|
+
x: dimensions.lane.x,
|
|
560
|
+
y: laneY,
|
|
561
|
+
width: dimensions.lane.width,
|
|
562
|
+
height: dimensions.lane.height,
|
|
563
|
+
tone: 'neutral'
|
|
564
|
+
});
|
|
565
|
+
yCursor += safeLaneRows * dimensions.rowHeight + dimensions.groupGap;
|
|
566
|
+
}
|
|
567
|
+
const height = Math.max(360, yCursor + dimensions.paddingY - dimensions.groupGap);
|
|
568
|
+
const width = dimensions.task.x + dimensions.task.width + 32;
|
|
569
|
+
const activeAgentCount = agentNodes.length;
|
|
570
|
+
const sourceKind = dashboardSourceKind(dashboard);
|
|
571
|
+
const sourceNode = {
|
|
572
|
+
id: 'source:global',
|
|
573
|
+
kind: 'source',
|
|
574
|
+
label: sourceKind === 'lifetime' ? 'Global swarm' : 'Current swarm',
|
|
575
|
+
detail: sourceKind === 'lifetime' ? 'Workspace lifetime' : 'Active run',
|
|
576
|
+
meta: `${text(activeAgentCount)} agents · ${text(scopedItems.length)} active tasks`,
|
|
577
|
+
x: dimensions.source.x,
|
|
578
|
+
y: Math.max(dimensions.paddingY, height / 2 - dimensions.source.height / 2),
|
|
579
|
+
width: dimensions.source.width,
|
|
580
|
+
height: dimensions.source.height,
|
|
581
|
+
tone: 'neutral'
|
|
582
|
+
};
|
|
583
|
+
nodes.push(sourceNode, ...laneNodes, ...agentNodes, ...taskNodes);
|
|
584
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
585
|
+
for (const lane of laneNodes) {
|
|
586
|
+
edges.push(runningGraphEdge(sourceNode, lane, 'neutral'));
|
|
587
|
+
const laneId = lane.id.slice('lane:'.length);
|
|
588
|
+
for (const agent of agentNodes.filter((node) => node.id.startsWith(`agent:${laneId}:`))) {
|
|
589
|
+
edges.push(runningGraphEdge(lane, agent, 'neutral'));
|
|
590
|
+
const workerKey = agent.id.slice(`agent:${laneId}:`.length);
|
|
591
|
+
const workerJobs = scopedItems.filter((job) => laneOf(job) === laneId && agentIdentityKey(job) === workerKey);
|
|
592
|
+
for (const job of workerJobs) {
|
|
593
|
+
const taskNode = byId.get(`task:${taskCardId(job)}`);
|
|
594
|
+
if (taskNode)
|
|
595
|
+
edges.push(runningGraphEdge(agent, taskNode, 'good'));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
nodes,
|
|
601
|
+
edges,
|
|
602
|
+
laneOptions,
|
|
603
|
+
taskOptions,
|
|
604
|
+
filter: { lane: laneFilter, task: taskFilter },
|
|
605
|
+
width,
|
|
606
|
+
height,
|
|
607
|
+
totalActiveJobCount: activeItems.length,
|
|
608
|
+
activeJobCount: scopedItems.length,
|
|
609
|
+
activeAgentCount
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function runningLaneCounts(jobs) {
|
|
613
|
+
const counts = new Map();
|
|
614
|
+
for (const job of jobs) {
|
|
615
|
+
const lane = laneOf(job);
|
|
616
|
+
counts.set(lane, (counts.get(lane) ?? 0) + 1);
|
|
617
|
+
}
|
|
618
|
+
return counts;
|
|
619
|
+
}
|
|
620
|
+
function laneDisplayLabel(id) {
|
|
621
|
+
if (!id || id === 'unassigned')
|
|
622
|
+
return 'Unassigned';
|
|
623
|
+
return sentenceCaseIdentifier(id);
|
|
624
|
+
}
|
|
625
|
+
function laneModelSummary(jobs) {
|
|
626
|
+
const models = uniqueStrings(jobs.map(agentModelLabel).filter((model) => model !== 'model unknown'));
|
|
627
|
+
if (!models.length)
|
|
628
|
+
return 'model unknown';
|
|
629
|
+
if (models.length === 1)
|
|
630
|
+
return models[0];
|
|
631
|
+
return `${models.slice(0, 2).join(' + ')}${models.length > 2 ? ` +${models.length - 2}` : ''}`;
|
|
632
|
+
}
|
|
633
|
+
function runningGraphEdge(from, to, tone) {
|
|
634
|
+
return {
|
|
635
|
+
id: `${from.id}->${to.id}`,
|
|
636
|
+
from: from.id,
|
|
637
|
+
to: to.id,
|
|
638
|
+
path: runningGraphEdgePath(from, to),
|
|
639
|
+
tone
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function runningGraphEdgePath(from, to) {
|
|
643
|
+
const x1 = from.x + from.width;
|
|
644
|
+
const y1 = from.y + from.height / 2;
|
|
645
|
+
const x2 = to.x;
|
|
646
|
+
const y2 = to.y + to.height / 2;
|
|
647
|
+
const curve = Math.max(38, Math.min(120, (x2 - x1) / 2));
|
|
648
|
+
return `M ${x1} ${y1} C ${x1 + curve} ${y1}, ${x2 - curve} ${y2}, ${x2} ${y2}`;
|
|
649
|
+
}
|
|
427
650
|
function AgentWork({ dashboard, jobs }) {
|
|
428
651
|
const items = taskBoardItems(dashboard, jobs).filter(isActiveAgentJob);
|
|
429
652
|
const workers = agentWorkers(items);
|
|
@@ -521,7 +744,7 @@ function AgentWorkerCard({ worker, index, now }) {
|
|
|
521
744
|
const model = agentModelSummary(worker);
|
|
522
745
|
const files = agentTouchedFiles(worker);
|
|
523
746
|
const evidenceCount = agentEvidenceCount(worker);
|
|
524
|
-
const fileFallback =
|
|
747
|
+
const fileFallback = agentFileFallback(worker, evidenceCount);
|
|
525
748
|
const visibleJobs = worker.currentJobs.slice(0, 2);
|
|
526
749
|
const hiddenJobCount = Math.max(0, worker.currentJobs.length - visibleJobs.length);
|
|
527
750
|
return _jsxs("article", { className: `agent-worker-card ${worker.status}`, style: `--agent-color:${worker.color}`, draggable: "false", "aria-label": `${agentDisplayName(worker, index)} assigned to ${text(worker.currentJobs.length)} current tasks`, children: [_jsxs("div", { className: "agent-cell agent-cell-agent", children: [_jsx("span", { className: "agent-status-dot", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("b", { children: agentDisplayName(worker, index) }), _jsx("small", { children: agentStatusLabel(worker.status) })] })] }), _jsx("div", { className: "agent-cell agent-cell-task", children: _jsxs("div", { className: "agent-task-list", children: [visibleJobs.map((job) => _jsxs("button", { type: "button", className: "agent-task-row", "data-task-card": taskCardId(job), children: [_jsx("code", { children: ticketId(job) }), _jsx("span", { children: taskTitle(job) }), _jsx("small", { children: jobRuntimeLabel(job, now) })] })), hiddenJobCount ? _jsxs("small", { className: "agent-overflow-note", children: ["+", text(hiddenJobCount), " more active ", hiddenJobCount === 1 ? 'ticket' : 'tickets'] }) : null] }) }), _jsx("div", { className: "agent-cell agent-cell-model", children: _jsx("span", { className: "agent-model-pill", children: _jsx("span", { children: model }) }) }), _jsxs("div", { className: "agent-cell agent-cell-runtime", children: [_jsx("b", { children: agentRuntimeValue(worker, now) }), _jsx("small", { children: worker.currentJobs.length === 1 ? 'current ticket' : `${text(worker.currentJobs.length)} tickets` })] }), _jsxs("div", { className: "agent-cell agent-cell-cost", children: [_jsx("b", { children: agentTokenValue(worker) }), _jsxs("small", { children: [agentUncachedTokenValue(worker), " uncached"] })] }), _jsxs("div", { className: "agent-cell agent-cell-files", children: [_jsxs("div", { className: "agent-file-list", children: [files.slice(0, 2).map((file) => _jsx(ArtifactLink, { path: file, label: artifactLabel(file), className: "agent-file-tag" })), files.length > 2 ? _jsxs("span", { className: "agent-file-more", children: ["+", text(files.length - 2)] }) : null, !files.length ? _jsx("span", { className: "agent-file-empty", children: fileFallback }) : null] }), evidenceCount ? _jsxs("small", { children: [text(evidenceCount), " evidence ", evidenceCount === 1 ? 'artifact' : 'artifacts'] }) : null] })] });
|
|
@@ -552,8 +775,8 @@ function TaskFileDiffs({ job, details }) {
|
|
|
552
775
|
return _jsxs("details", { className: "task-file-diff", "data-task-file-diff-key": keys[0] ?? '', "data-task-file-diff-keys": keys.join('\t'), open: isTaskFileDiffOpen(job, file) ? 'open' : undefined, children: [_jsxs("summary", { children: [_jsx("span", { className: "task-file-name", children: file.path }), _jsxs("small", { children: ["+", text(file.additions), " -", text(file.deletions), file.truncated ? ' · truncated' : ''] }), _jsx(ArtifactLink, { path: file.artifactPath ?? file.path, label: "Reveal", className: "task-file-reveal" })] }), _jsx(DiffRenderer, { file: file })] });
|
|
553
776
|
}) })] });
|
|
554
777
|
if (changedPaths.length)
|
|
555
|
-
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("p", { className: "empty tight", children:
|
|
556
|
-
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("p", { className: "empty tight", children: job
|
|
778
|
+
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("p", { className: "empty tight", children: fileDiffFallbackText(job, details) }), _jsx(ArtifactPathList, { paths: changedPaths })] });
|
|
779
|
+
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("p", { className: "empty tight", children: noChangedFilesText(job) })] });
|
|
557
780
|
}
|
|
558
781
|
function TaskResultDetails({ job, details }) {
|
|
559
782
|
const evidenceArtifacts = artifactListFromDetails(job, details).slice(0, 10);
|
|
@@ -981,6 +1204,16 @@ function agentEvidenceCount(worker) {
|
|
|
981
1204
|
return paths.length;
|
|
982
1205
|
return worker.jobs.reduce((sum, job) => sum + numberValue(job.evidencePathCount), 0);
|
|
983
1206
|
}
|
|
1207
|
+
function agentFileFallback(worker, evidenceCount) {
|
|
1208
|
+
const summaries = worker.jobs.map(sourceOutputSummary);
|
|
1209
|
+
if (summaries.some((summary) => summary.state === 'recovered-patch-failed'))
|
|
1210
|
+
return 'Patch failed';
|
|
1211
|
+
if (summaries.some((summary) => summary.state === 'recovered-patch'))
|
|
1212
|
+
return 'Recovered patch';
|
|
1213
|
+
if (evidenceCount)
|
|
1214
|
+
return 'Evidence only';
|
|
1215
|
+
return worker.status === 'active' ? 'Files pending' : 'No source files';
|
|
1216
|
+
}
|
|
984
1217
|
function agentWorkerSort(left, right) {
|
|
985
1218
|
return agentStatusRank(agentStatus(left)) - agentStatusRank(agentStatus(right))
|
|
986
1219
|
|| taskBoardJobSort(left, right);
|
|
@@ -1119,9 +1352,11 @@ function taskDetailSummary(job) {
|
|
|
1119
1352
|
function taskMarkdownSummary(job) {
|
|
1120
1353
|
const changed = formatNumber(numberValue(job.changedPathCount));
|
|
1121
1354
|
const evidence = formatNumber(numberValue(job.evidencePathCount));
|
|
1355
|
+
const sourceOutput = sourceOutputSummary(job);
|
|
1122
1356
|
const reasons = taskReasonItems(job);
|
|
1123
1357
|
const reasonText = reasons.length ? ` Main signal: ${reasons[0]}.` : '';
|
|
1124
|
-
|
|
1358
|
+
const sourceText = sourceOutput.detail ? ` ${sourceOutput.detail}` : '';
|
|
1359
|
+
return `${taskTitle(job)} is in ${taskBoardColumnTitle(taskBoardColumnId(job)).toLowerCase()} with ${changed} changed paths and ${evidence} evidence artifacts.${sourceText}${reasonText}`;
|
|
1125
1360
|
}
|
|
1126
1361
|
function coordinatorDecisionStatus(job) {
|
|
1127
1362
|
return textValue(job.coordinatorDecisionStatus ?? recordValue(job.coordinatorDecision).status, '');
|
|
@@ -2433,20 +2668,124 @@ function pathSummaryText(job) {
|
|
|
2433
2668
|
const changedPathCount = numberValue(job.changedPathCount);
|
|
2434
2669
|
const evidencePathCount = numberValue(job.evidencePathCount);
|
|
2435
2670
|
const ignoredCount = ignoredOwnership.length + ignoredChangedPathCount;
|
|
2671
|
+
const sourceOutput = sourceOutputSummary(job);
|
|
2436
2672
|
if (sourceOwnership.length)
|
|
2437
2673
|
return `${text(sourceOwnership.length)} source issue`;
|
|
2438
2674
|
if (quarantinedChangedPathCount)
|
|
2439
2675
|
return `${text(quarantinedChangedPathCount)} quarantined`;
|
|
2440
2676
|
if (ignoredCount)
|
|
2441
2677
|
return `${text(ignoredCount)} generated noise`;
|
|
2678
|
+
if (sourceOutput.state === 'recovered-patch' || sourceOutput.state === 'recovered-patch-failed')
|
|
2679
|
+
return sourceOutput.label;
|
|
2442
2680
|
if (!changedPathCount && hasPatchArtifact(job))
|
|
2443
2681
|
return 'patch not indexed yet';
|
|
2444
2682
|
if (!changedPathCount && evidencePathCount)
|
|
2445
|
-
return
|
|
2683
|
+
return sourceOutput.label;
|
|
2446
2684
|
if (!changedPathCount && isActiveAgentJob(job))
|
|
2447
2685
|
return 'files pending';
|
|
2448
2686
|
return `${text(changedPathCount)} changed ${changedPathCount === 1 ? 'path' : 'paths'}`;
|
|
2449
2687
|
}
|
|
2688
|
+
function sourceOutputSummary(job) {
|
|
2689
|
+
const explicitState = normalized(job.sourceOutputState);
|
|
2690
|
+
const explicitLabel = textValue(job.sourceOutputLabel, '');
|
|
2691
|
+
const explicitDetail = textValue(job.sourceOutputDetail, '');
|
|
2692
|
+
if (explicitState)
|
|
2693
|
+
return {
|
|
2694
|
+
state: sourceOutputStateValue(explicitState),
|
|
2695
|
+
label: explicitLabel || sentenceCaseIdentifier(explicitState),
|
|
2696
|
+
detail: explicitDetail
|
|
2697
|
+
};
|
|
2698
|
+
const changedPathCount = numberValue(job.changedPathCount) || stringArray(job.changedPaths).length;
|
|
2699
|
+
const evidencePathCount = numberValue(job.evidencePathCount) || stringArray(job.evidencePaths).length;
|
|
2700
|
+
const hasPatch = hasPatchArtifact(job);
|
|
2701
|
+
const signals = sourceOutputSignals(job);
|
|
2702
|
+
const recoveryStatus = sourceOutputRecoveryStatus(job);
|
|
2703
|
+
const workspaceRecovery = recoveryStatus !== '' || signals.some((signal) => signal.includes('collector-workspace-only-recovery'));
|
|
2704
|
+
const failedPatch = recoveryStatus === 'failed-patch'
|
|
2705
|
+
|| signals.some((signal) => signal.includes('collector-workspace-only-recovery-failed-patch') || signal === 'empty patch' || signal === 'empty-patch');
|
|
2706
|
+
if (failedPatch)
|
|
2707
|
+
return {
|
|
2708
|
+
state: 'recovered-patch-failed',
|
|
2709
|
+
label: changedPathCount ? `${text(changedPathCount)} ${changedPathCount === 1 ? 'path' : 'paths'}, patch failed` : 'patch generation failed',
|
|
2710
|
+
detail: changedPathCount
|
|
2711
|
+
? 'Source changed, but recovered patch generation failed.'
|
|
2712
|
+
: 'Patch generation failed before a source diff could be attached.'
|
|
2713
|
+
};
|
|
2714
|
+
if (workspaceRecovery && (hasPatch || changedPathCount))
|
|
2715
|
+
return {
|
|
2716
|
+
state: 'recovered-patch',
|
|
2717
|
+
label: changedPathCount ? `${text(changedPathCount)} recovered ${changedPathCount === 1 ? 'path' : 'paths'}` : 'recovered patch',
|
|
2718
|
+
detail: changedPathCount
|
|
2719
|
+
? 'Source changed and the collector recovered a patch from the worker workspace.'
|
|
2720
|
+
: 'The collector recovered a patch, but changed paths are not indexed yet.'
|
|
2721
|
+
};
|
|
2722
|
+
if (!changedPathCount && hasPatch)
|
|
2723
|
+
return {
|
|
2724
|
+
state: 'patch-unindexed',
|
|
2725
|
+
label: 'patch not indexed yet',
|
|
2726
|
+
detail: 'A patch artifact exists, but changed paths are not indexed yet.'
|
|
2727
|
+
};
|
|
2728
|
+
if (!changedPathCount && evidencePathCount)
|
|
2729
|
+
return {
|
|
2730
|
+
state: 'evidence-only',
|
|
2731
|
+
label: 'evidence only',
|
|
2732
|
+
detail: 'No source files changed; this task produced evidence only.'
|
|
2733
|
+
};
|
|
2734
|
+
if (!changedPathCount)
|
|
2735
|
+
return {
|
|
2736
|
+
state: 'no-source-files',
|
|
2737
|
+
label: 'no source files',
|
|
2738
|
+
detail: 'No source files are reported for this task.'
|
|
2739
|
+
};
|
|
2740
|
+
return { state: '', label: '', detail: '' };
|
|
2741
|
+
}
|
|
2742
|
+
function sourceOutputStateValue(value) {
|
|
2743
|
+
if (value === 'recovered-patch')
|
|
2744
|
+
return 'recovered-patch';
|
|
2745
|
+
if (value === 'recovered-patch-failed')
|
|
2746
|
+
return 'recovered-patch-failed';
|
|
2747
|
+
if (value === 'patch-unindexed')
|
|
2748
|
+
return 'patch-unindexed';
|
|
2749
|
+
if (value === 'evidence-only')
|
|
2750
|
+
return 'evidence-only';
|
|
2751
|
+
if (value === 'no-source-files')
|
|
2752
|
+
return 'no-source-files';
|
|
2753
|
+
return '';
|
|
2754
|
+
}
|
|
2755
|
+
function sourceOutputSignals(job) {
|
|
2756
|
+
return uniqueStrings([
|
|
2757
|
+
...stringArray(job.collectReasonClasses),
|
|
2758
|
+
...stringArray(job.reasons),
|
|
2759
|
+
...stringArray(job.semanticReadinessReasons),
|
|
2760
|
+
textValue(job.disposition, ''),
|
|
2761
|
+
textValue(job.mergeReadiness, ''),
|
|
2762
|
+
textValue(job.status, '')
|
|
2763
|
+
].map(normalized));
|
|
2764
|
+
}
|
|
2765
|
+
function sourceOutputRecoveryStatus(job) {
|
|
2766
|
+
const metadata = recordValue(job.metadata);
|
|
2767
|
+
const swarmCodex = recordValue(metadata.frontierSwarmCodex);
|
|
2768
|
+
const workspaceOnly = recordValue(swarmCodex.workspaceOnlyCollection);
|
|
2769
|
+
return normalized(workspaceOnly.recoveryStatus);
|
|
2770
|
+
}
|
|
2771
|
+
function fileDiffFallbackText(job, details) {
|
|
2772
|
+
if (details?.error)
|
|
2773
|
+
return `Diff unavailable: ${details.error}`;
|
|
2774
|
+
const sourceOutput = sourceOutputSummary(job);
|
|
2775
|
+
if (sourceOutput.state === 'recovered-patch-failed')
|
|
2776
|
+
return 'Source changed, but recovered patch generation failed. Review the listed paths and evidence.';
|
|
2777
|
+
if (sourceOutput.state === 'recovered-patch')
|
|
2778
|
+
return 'Recovered source paths are known, but patch text is not available in the dashboard.';
|
|
2779
|
+
return 'No patch diff was available in the collected evidence.';
|
|
2780
|
+
}
|
|
2781
|
+
function noChangedFilesText(job) {
|
|
2782
|
+
const sourceOutput = sourceOutputSummary(job);
|
|
2783
|
+
if (sourceOutput.state === 'evidence-only')
|
|
2784
|
+
return 'No source files changed; this task produced evidence only.';
|
|
2785
|
+
if (job.boardKind === 'backlog')
|
|
2786
|
+
return 'No files yet. This item has not produced an implementation patch.';
|
|
2787
|
+
return 'No changed files reported.';
|
|
2788
|
+
}
|
|
2450
2789
|
function hasPatchArtifact(job) {
|
|
2451
2790
|
const paths = [
|
|
2452
2791
|
textValue(job.patchPath, ''),
|
|
@@ -2751,6 +3090,8 @@ function tabMeta(tab, input) {
|
|
|
2751
3090
|
return `${text(input.jobs)} tasks`;
|
|
2752
3091
|
if (tab === 'board')
|
|
2753
3092
|
return 'AI tasks';
|
|
3093
|
+
if (tab === 'running')
|
|
3094
|
+
return 'live graph';
|
|
2754
3095
|
if (tab === 'swarm')
|
|
2755
3096
|
return 'active agents';
|
|
2756
3097
|
if (tab === 'lanes')
|
|
@@ -4011,25 +4352,369 @@ function semanticMetrics(value) {
|
|
|
4011
4352
|
const script = recordValue(edit.script);
|
|
4012
4353
|
const replay = recordValue(input.replay);
|
|
4013
4354
|
const admission = recordValue(input.admission);
|
|
4355
|
+
const health = recordValue(input.health);
|
|
4356
|
+
const parser = recordValue(health.parser);
|
|
4357
|
+
const ledger = recordValue(health.ledger);
|
|
4358
|
+
const merge = recordValue(health.merge);
|
|
4359
|
+
const gates = recordValue(health.gates);
|
|
4360
|
+
const outcomes = recordValue(health.outcomes);
|
|
4361
|
+
const admissionHealth = recordValue(health.admission);
|
|
4014
4362
|
const admissionRows = [
|
|
4015
4363
|
...semanticAdmissionRows(admission.jobs, 'Jobs'),
|
|
4016
4364
|
...semanticAdmissionRows(admission.scripts, 'Scripts')
|
|
4017
4365
|
];
|
|
4018
4366
|
const metrics = {
|
|
4019
|
-
expected:
|
|
4020
|
-
satisfied:
|
|
4021
|
-
candidates:
|
|
4022
|
-
autoMerge:
|
|
4023
|
-
acceptedClean:
|
|
4024
|
-
conflicts:
|
|
4367
|
+
expected: firstNumber(imports.expectedCount, input.expected),
|
|
4368
|
+
satisfied: firstNumber(imports.expectedSatisfiedCount, input.satisfied),
|
|
4369
|
+
candidates: firstNumber(imports.candidateCount, input.candidates),
|
|
4370
|
+
autoMerge: firstNumber(script.autoMergeCandidateCount, input.autoMerge),
|
|
4371
|
+
acceptedClean: firstNumber(replay.acceptedCleanCount, input.acceptedClean),
|
|
4372
|
+
conflicts: firstNumber(merge.conflictCount, replay.conflictCount, input.conflicts)
|
|
4373
|
+
};
|
|
4374
|
+
const admissionReasonCodeCounts = numberRecordValue(recordValue(admissionHealth.reasonCodeCounts));
|
|
4375
|
+
const admissionStatusCounts = semanticAdmissionStatusCounts(admission, admissionHealth, {
|
|
4376
|
+
safeMerged: Math.max(metrics.autoMerge, metrics.acceptedClean, numberValue(replay.alreadyAppliedCount), numberValue(ledger.landedCount)),
|
|
4377
|
+
safeWithLosses: numberValue(admissionReasonCodeCounts['lossy-import']) || firstNumber(parser.lossCount, imports.lossCount),
|
|
4378
|
+
conflict: metrics.conflicts,
|
|
4379
|
+
stale: numberValue(merge.staleCount) || numberValue(admissionReasonCodeCounts['stale-source-hash']),
|
|
4380
|
+
missingSidecar: numberValue(admissionReasonCodeCounts['missing-sidecar']),
|
|
4381
|
+
coordinatorReview: Math.max(numberValue(merge.reviewRequiredCount), numberValue(outcomes.openCoordinatorReviewCount)),
|
|
4382
|
+
testsMissing: numberValue(admissionReasonCodeCounts['tests-missing'])
|
|
4383
|
+
});
|
|
4384
|
+
const semanticHealth = {
|
|
4385
|
+
parserLosses: firstNumber(parser.lossCount, imports.lossCount),
|
|
4386
|
+
parserWarnings: firstNumber(parser.warningCount, imports.warningCount),
|
|
4387
|
+
ledgerFailed: numberValue(ledger.failedCount),
|
|
4388
|
+
ledgerSkipped: numberValue(ledger.skippedCount),
|
|
4389
|
+
ledgerLanded: numberValue(ledger.landedCount),
|
|
4390
|
+
autoMergeCandidates: firstNumber(merge.autoMergeCandidateCount, metrics.autoMerge),
|
|
4391
|
+
reviewRequired: numberValue(merge.reviewRequiredCount),
|
|
4392
|
+
conflicts: metrics.conflicts,
|
|
4393
|
+
gateStatus: textValue(gates.status, 'unknown'),
|
|
4394
|
+
gatePassed: numberValue(gates.passedCount),
|
|
4395
|
+
gateWarnings: numberValue(gates.warningCount),
|
|
4396
|
+
gateFailed: numberValue(gates.failedCount),
|
|
4397
|
+
synthesizedResearchComplete: numberValue(outcomes.synthesizedResearchCompleteCount),
|
|
4398
|
+
openCoordinatorReview: numberValue(outcomes.openCoordinatorReviewCount),
|
|
4399
|
+
admissionStatusCounts,
|
|
4400
|
+
admissionReasonCodeCounts,
|
|
4401
|
+
reasonCodes: uniqueStrings([
|
|
4402
|
+
...stringArray(parser.expectedMissingReasonCodes),
|
|
4403
|
+
...stringArray(merge.reasonCodes),
|
|
4404
|
+
...stringArray(gates.reasonCodes)
|
|
4405
|
+
]).slice(0, 6)
|
|
4025
4406
|
};
|
|
4026
4407
|
return {
|
|
4027
4408
|
...metrics,
|
|
4028
|
-
total: metrics.expected + metrics.satisfied + metrics.candidates + metrics.autoMerge + metrics.acceptedClean + metrics.conflicts
|
|
4409
|
+
total: metrics.expected + metrics.satisfied + metrics.candidates + metrics.autoMerge + metrics.acceptedClean + metrics.conflicts +
|
|
4410
|
+
semanticHealth.parserLosses + semanticHealth.reviewRequired,
|
|
4029
4411
|
admissionRows,
|
|
4030
|
-
admissionTotal: admissionRows.reduce((sum, row) => sum + row.value, 0)
|
|
4412
|
+
admissionTotal: admissionRows.reduce((sum, row) => sum + row.value, 0),
|
|
4413
|
+
health: semanticHealth
|
|
4414
|
+
};
|
|
4415
|
+
}
|
|
4416
|
+
function decisionGraphSummary(dashboard) {
|
|
4417
|
+
const graph = recordValue(dashboard.graph ?? recordValue(dashboard.summary).graph);
|
|
4418
|
+
const nodeCount = numberValue(graph.nodeCount);
|
|
4419
|
+
const edgeCount = numberValue(graph.edgeCount);
|
|
4420
|
+
const blockerCount = numberValue(graph.blockerCount);
|
|
4421
|
+
const humanQuestionCount = numberValue(graph.humanQuestionCount);
|
|
4422
|
+
const safeMergeCandidateCount = numberValue(graph.safeMergeCandidateCount);
|
|
4423
|
+
const decisionCount = numberValue(graph.decisionCount);
|
|
4424
|
+
const terminalDecisionCount = numberValue(graph.terminalDecisionCount);
|
|
4425
|
+
const gateCount = numberValue(graph.gateCount);
|
|
4426
|
+
const recentEvents = arrayRecords(graph.recentEvents).map((event) => ({
|
|
4427
|
+
type: textValue(event.type, ''),
|
|
4428
|
+
at: timeValue(event.at),
|
|
4429
|
+
jobId: textValue(event.jobId, ''),
|
|
4430
|
+
taskId: textValue(event.taskId, ''),
|
|
4431
|
+
status: textValue(event.status, ''),
|
|
4432
|
+
message: textValue(event.message, '')
|
|
4433
|
+
}));
|
|
4434
|
+
const graphMissingWarnings = stringArray(graph.graphMissingWarnings);
|
|
4435
|
+
if (!nodeCount && !edgeCount && !blockerCount && !humanQuestionCount && !safeMergeCandidateCount && !decisionCount && !terminalDecisionCount && !gateCount && !recentEvents.length && !graphMissingWarnings.length) {
|
|
4436
|
+
return undefined;
|
|
4437
|
+
}
|
|
4438
|
+
return {
|
|
4439
|
+
sourceFile: textValue(graph.sourceFile, ''),
|
|
4440
|
+
sourceFiles: stringArray(graph.sourceFiles),
|
|
4441
|
+
sourceKind: textValue(graph.sourceKind, ''),
|
|
4442
|
+
sourceKinds: stringArray(graph.sourceKinds),
|
|
4443
|
+
sourceStatus: textValue(graph.sourceStatus, ''),
|
|
4444
|
+
sourceStatuses: stringArray(graph.sourceStatuses),
|
|
4445
|
+
graphMissing: graph.graphMissing === true,
|
|
4446
|
+
graphMissingWarningCount: numberValue(graph.graphMissingWarningCount),
|
|
4447
|
+
graphMissingWarnings,
|
|
4448
|
+
liveEventCount: numberValue(graph.liveEventCount),
|
|
4449
|
+
nodeCount,
|
|
4450
|
+
edgeCount,
|
|
4451
|
+
blockerCount,
|
|
4452
|
+
openBlockerCount: numberValue(graph.openBlockerCount),
|
|
4453
|
+
humanQuestionCount,
|
|
4454
|
+
openHumanQuestionCount: numberValue(graph.openHumanQuestionCount),
|
|
4455
|
+
safeMergeCandidateCount,
|
|
4456
|
+
decisionCount,
|
|
4457
|
+
terminalDecisionCount,
|
|
4458
|
+
terminalAcceptedCount: numberValue(graph.terminalAcceptedCount),
|
|
4459
|
+
terminalRejectedCount: numberValue(graph.terminalRejectedCount),
|
|
4460
|
+
terminalRerunCount: numberValue(graph.terminalRerunCount),
|
|
4461
|
+
gateCount,
|
|
4462
|
+
gatePassedCount: numberValue(graph.gatePassedCount),
|
|
4463
|
+
gateFailedCount: numberValue(graph.gateFailedCount),
|
|
4464
|
+
staleCount: numberValue(graph.staleCount),
|
|
4465
|
+
openStaleCount: numberValue(graph.openStaleCount),
|
|
4466
|
+
rerunCount: numberValue(graph.rerunCount),
|
|
4467
|
+
openRerunCount: numberValue(graph.openRerunCount),
|
|
4468
|
+
staleRerunCleanupCount: numberValue(graph.staleRerunCleanupCount),
|
|
4469
|
+
status: textValue(graph.status, 'unknown'),
|
|
4470
|
+
summaryLine: textValue(graph.summaryLine, ''),
|
|
4471
|
+
recentEvents
|
|
4031
4472
|
};
|
|
4032
4473
|
}
|
|
4474
|
+
function decisionGraphRows(graph) {
|
|
4475
|
+
const sourceKinds = (graph.sourceKinds?.length ? graph.sourceKinds : [graph.sourceKind]).filter(Boolean).join(', ') || 'unknown source';
|
|
4476
|
+
const sourceStatuses = (graph.sourceStatuses?.length ? graph.sourceStatuses : [graph.sourceStatus]).filter(Boolean).join(', ') || 'unknown status';
|
|
4477
|
+
const warnings = graph.graphMissingWarnings?.slice(0, 2).join(' ') || '';
|
|
4478
|
+
const recent = graph.recentEvents?.slice(-3).map((event) => {
|
|
4479
|
+
const label = event.message || event.type || 'graph event';
|
|
4480
|
+
const subject = event.taskId || event.jobId;
|
|
4481
|
+
return `${label}${subject ? ` (${subject})` : ''}`;
|
|
4482
|
+
}).filter(Boolean).join('; ') || 'no recent graph events';
|
|
4483
|
+
return [
|
|
4484
|
+
{
|
|
4485
|
+
label: 'Decision graph source',
|
|
4486
|
+
value: graphSourceStatusLabel(graph),
|
|
4487
|
+
detail: warnings || `${sourceKinds} · ${sourceStatuses}${graph.liveEventCount ? ` · ${formatNumber(graph.liveEventCount)} live events` : ''}`
|
|
4488
|
+
},
|
|
4489
|
+
{
|
|
4490
|
+
label: 'Graph shape',
|
|
4491
|
+
value: `${formatNumber(numberValue(graph.nodeCount))} / ${formatNumber(numberValue(graph.edgeCount))}`,
|
|
4492
|
+
detail: graph.summaryLine || `${formatNumber(numberValue(graph.nodeCount))} nodes and ${formatNumber(numberValue(graph.edgeCount))} edges`
|
|
4493
|
+
},
|
|
4494
|
+
{
|
|
4495
|
+
label: 'Terminal decisions',
|
|
4496
|
+
value: formatNumber(numberValue(graph.terminalDecisionCount)),
|
|
4497
|
+
detail: `${formatNumber(numberValue(graph.terminalAcceptedCount))} accepted · ${formatNumber(numberValue(graph.terminalRejectedCount))} rejected · ${formatNumber(numberValue(graph.terminalRerunCount))} rerun`
|
|
4498
|
+
},
|
|
4499
|
+
{
|
|
4500
|
+
label: 'Open blockers',
|
|
4501
|
+
value: formatNumber(numberValue(graph.openBlockerCount)),
|
|
4502
|
+
detail: `${formatNumber(numberValue(graph.blockerCount))} blockers tracked`
|
|
4503
|
+
},
|
|
4504
|
+
{
|
|
4505
|
+
label: 'Human questions',
|
|
4506
|
+
value: formatNumber(numberValue(graph.openHumanQuestionCount)),
|
|
4507
|
+
detail: `${formatNumber(numberValue(graph.humanQuestionCount))} questions tracked`
|
|
4508
|
+
},
|
|
4509
|
+
{
|
|
4510
|
+
label: 'Stale/rerun cleanup',
|
|
4511
|
+
value: formatNumber(numberValue(graph.staleRerunCleanupCount)),
|
|
4512
|
+
detail: `${formatNumber(numberValue(graph.openStaleCount))} stale open · ${formatNumber(numberValue(graph.openRerunCount))} rerun open`
|
|
4513
|
+
},
|
|
4514
|
+
{
|
|
4515
|
+
label: 'Safe merge candidates',
|
|
4516
|
+
value: formatNumber(numberValue(graph.safeMergeCandidateCount)),
|
|
4517
|
+
detail: `${formatNumber(numberValue(graph.decisionCount))} decisions · ${formatNumber(numberValue(graph.gateCount))} gates`
|
|
4518
|
+
},
|
|
4519
|
+
{
|
|
4520
|
+
label: 'Recent graph events',
|
|
4521
|
+
value: formatNumber(graph.recentEvents?.length ?? 0),
|
|
4522
|
+
detail: recent
|
|
4523
|
+
}
|
|
4524
|
+
];
|
|
4525
|
+
}
|
|
4526
|
+
function graphSourceStatusLabel(graph) {
|
|
4527
|
+
const status = normalized(graph.sourceStatus);
|
|
4528
|
+
if (graph.graphMissing || status === 'missing')
|
|
4529
|
+
return 'Graph missing warning';
|
|
4530
|
+
if (status === 'live')
|
|
4531
|
+
return 'Live graph';
|
|
4532
|
+
if (status === 'collected')
|
|
4533
|
+
return 'Collected graph';
|
|
4534
|
+
if (status === 'mixed')
|
|
4535
|
+
return 'Mixed graph';
|
|
4536
|
+
return 'Graph source';
|
|
4537
|
+
}
|
|
4538
|
+
function decisionGraphStatusLabel(value) {
|
|
4539
|
+
const status = normalized(value);
|
|
4540
|
+
if (status === 'blocked')
|
|
4541
|
+
return 'Blocked';
|
|
4542
|
+
if (status === 'questions')
|
|
4543
|
+
return 'Questions';
|
|
4544
|
+
if (status === 'review')
|
|
4545
|
+
return 'Review';
|
|
4546
|
+
if (status === 'rerun')
|
|
4547
|
+
return 'Rerun';
|
|
4548
|
+
if (status === 'missing')
|
|
4549
|
+
return 'Missing';
|
|
4550
|
+
if (status === 'ready')
|
|
4551
|
+
return 'Ready';
|
|
4552
|
+
if (status === 'clear')
|
|
4553
|
+
return 'Clear';
|
|
4554
|
+
return 'Unknown';
|
|
4555
|
+
}
|
|
4556
|
+
function semanticHealthRows(semantic) {
|
|
4557
|
+
const health = semantic.health;
|
|
4558
|
+
const ledgerLosses = health.ledgerFailed + health.ledgerSkipped;
|
|
4559
|
+
const reasonDetail = health.reasonCodes.length ? health.reasonCodes.join(', ') : 'no reason codes';
|
|
4560
|
+
return [
|
|
4561
|
+
{ label: 'Parser losses', value: formatNumber(health.parserLosses), detail: `${formatNumber(health.parserWarnings)} warnings · ${reasonDetail}` },
|
|
4562
|
+
{ label: 'Ledger losses', value: formatNumber(ledgerLosses), detail: `${formatNumber(health.ledgerFailed)} failed · ${formatNumber(health.ledgerSkipped)} skipped · ${formatNumber(health.ledgerLanded)} landed` },
|
|
4563
|
+
{ label: 'Auto-merge candidates', value: formatNumber(health.autoMergeCandidates), detail: `${formatNumber(semantic.acceptedClean)} replay clean` },
|
|
4564
|
+
...semanticAdmissionHealthRows(semantic),
|
|
4565
|
+
{ label: 'Review-required reasons', value: formatNumber(health.reviewRequired), detail: reasonDetail },
|
|
4566
|
+
{ label: 'Conflicts', value: formatNumber(health.conflicts), detail: 'semantic script, replay, or admission conflicts' },
|
|
4567
|
+
{ label: 'Gate status', value: semanticGateLabel(health.gateStatus), detail: `${formatNumber(health.gateFailed)} failed · ${formatNumber(health.gateWarnings)} review · ${formatNumber(health.gatePassed)} pass` },
|
|
4568
|
+
{ label: 'Synthesized/research complete', value: formatNumber(health.synthesizedResearchComplete), detail: 'completed discovery or synthesized outputs' },
|
|
4569
|
+
{ label: 'Open coordinator review', value: formatNumber(health.openCoordinatorReview), detail: 'outputs still waiting on coordinator decision' }
|
|
4570
|
+
];
|
|
4571
|
+
}
|
|
4572
|
+
const SEMANTIC_ADMISSION_STATUS_KEYS = [
|
|
4573
|
+
'safe-merged',
|
|
4574
|
+
'safe-with-losses',
|
|
4575
|
+
'conflict',
|
|
4576
|
+
'no-op',
|
|
4577
|
+
'stale',
|
|
4578
|
+
'missing-sidecar',
|
|
4579
|
+
'coordinator-review',
|
|
4580
|
+
'tests-missing'
|
|
4581
|
+
];
|
|
4582
|
+
const SEMANTIC_ADMISSION_STATUS_ROWS = [
|
|
4583
|
+
{ key: 'safe-merged', label: 'Safe merged', detail: 'semantic admission passed or replay is already clean', reasonCodes: [] },
|
|
4584
|
+
{ key: 'safe-with-losses', label: 'Safe with losses', detail: 'safe admission with parser or import losses attached', reasonCodes: ['lossy-import'] },
|
|
4585
|
+
{ key: 'conflict', label: 'Conflict', detail: 'symbol, effect, replay, or admission conflicts', reasonCodes: ['symbol-conflict', 'effect-conflict'] },
|
|
4586
|
+
{ key: 'no-op', label: 'No-op', detail: 'semantic evidence produced no source edit to apply', reasonCodes: [] },
|
|
4587
|
+
{ key: 'stale', label: 'Stale', detail: 'semantic anchor or source hash is stale against head', reasonCodes: ['stale-source-hash'] },
|
|
4588
|
+
{ key: 'missing-sidecar', label: 'Missing sidecar', detail: 'semantic import sidecar was expected but unavailable', reasonCodes: ['missing-sidecar'] },
|
|
4589
|
+
{ key: 'coordinator-review', label: 'Coordinator review', detail: 'semantic admission requires coordinator review or porting', reasonCodes: ['coordinator-review'] },
|
|
4590
|
+
{ key: 'tests-missing', label: 'Tests missing', detail: 'patch changed source without reported verification commands', reasonCodes: ['tests-missing'] }
|
|
4591
|
+
];
|
|
4592
|
+
function semanticAdmissionHealthRows(semantic) {
|
|
4593
|
+
return SEMANTIC_ADMISSION_STATUS_ROWS.map((row) => ({
|
|
4594
|
+
label: row.label,
|
|
4595
|
+
value: formatNumber(numberValue(semantic.health.admissionStatusCounts[row.key])),
|
|
4596
|
+
detail: admissionReasonDetail(semantic.health.admissionReasonCodeCounts, row.reasonCodes, row.detail)
|
|
4597
|
+
}));
|
|
4598
|
+
}
|
|
4599
|
+
function admissionReasonDetail(reasonCounts, reasonCodes, fallback) {
|
|
4600
|
+
const reasons = reasonCodes
|
|
4601
|
+
.map((code) => {
|
|
4602
|
+
const count = numberValue(reasonCounts[code]);
|
|
4603
|
+
return count > 0 ? `${code} ${formatNumber(count)}` : '';
|
|
4604
|
+
})
|
|
4605
|
+
.filter(Boolean);
|
|
4606
|
+
return reasons.length ? `${fallback} · ${reasons.join(', ')}` : fallback;
|
|
4607
|
+
}
|
|
4608
|
+
function semanticAdmissionStatusCounts(admission, admissionHealth, fallback) {
|
|
4609
|
+
const counts = emptySemanticAdmissionStatusCounts();
|
|
4610
|
+
const healthCounts = numberRecordValue(recordValue(admissionHealth.statusCounts));
|
|
4611
|
+
if (Object.keys(healthCounts).length) {
|
|
4612
|
+
addSemanticAdmissionStatusCounts(counts, healthCounts);
|
|
4613
|
+
}
|
|
4614
|
+
else {
|
|
4615
|
+
addSemanticAdmissionStatusCounts(counts, numberRecordValue(recordValue(recordValue(admission.jobs).statusCounts)));
|
|
4616
|
+
addSemanticAdmissionStatusCounts(counts, numberRecordValue(recordValue(recordValue(admission.scripts).statusCounts)));
|
|
4617
|
+
}
|
|
4618
|
+
setRecordMinimum(counts, 'safe-merged', fallback.safeMerged);
|
|
4619
|
+
setRecordMinimum(counts, 'safe-with-losses', fallback.safeWithLosses);
|
|
4620
|
+
setRecordMinimum(counts, 'conflict', fallback.conflict);
|
|
4621
|
+
setRecordMinimum(counts, 'stale', fallback.stale);
|
|
4622
|
+
setRecordMinimum(counts, 'missing-sidecar', fallback.missingSidecar);
|
|
4623
|
+
setRecordMinimum(counts, 'coordinator-review', fallback.coordinatorReview);
|
|
4624
|
+
setRecordMinimum(counts, 'tests-missing', fallback.testsMissing);
|
|
4625
|
+
return counts;
|
|
4626
|
+
}
|
|
4627
|
+
function emptySemanticAdmissionStatusCounts() {
|
|
4628
|
+
return SEMANTIC_ADMISSION_STATUS_KEYS.reduce((out, key) => {
|
|
4629
|
+
out[key] = 0;
|
|
4630
|
+
return out;
|
|
4631
|
+
}, {});
|
|
4632
|
+
}
|
|
4633
|
+
function addSemanticAdmissionStatusCounts(out, counts) {
|
|
4634
|
+
for (const [status, count] of Object.entries(counts)) {
|
|
4635
|
+
const key = semanticAdmissionStateKey(status);
|
|
4636
|
+
if (key && count > 0)
|
|
4637
|
+
out[key] += count;
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
function semanticAdmissionStateKey(value) {
|
|
4641
|
+
const signal = normalizedStatusKey(value);
|
|
4642
|
+
if (!signal)
|
|
4643
|
+
return undefined;
|
|
4644
|
+
if (signal === 'safe-merged' ||
|
|
4645
|
+
signal === 'safe' ||
|
|
4646
|
+
signal === 'accepted' ||
|
|
4647
|
+
signal === 'accepted-clean' ||
|
|
4648
|
+
signal === 'clean' ||
|
|
4649
|
+
signal === 'ready' ||
|
|
4650
|
+
signal === 'pass' ||
|
|
4651
|
+
signal === 'auto-merge-candidate' ||
|
|
4652
|
+
signal === 'already-applied' ||
|
|
4653
|
+
signal === 'portable')
|
|
4654
|
+
return 'safe-merged';
|
|
4655
|
+
if (signal === 'safe-with-losses' ||
|
|
4656
|
+
signal === 'lossy' ||
|
|
4657
|
+
signal === 'lossy-import' ||
|
|
4658
|
+
signal === 'warning' ||
|
|
4659
|
+
signal === 'warnings' ||
|
|
4660
|
+
signal === 'clean-with-losses')
|
|
4661
|
+
return 'safe-with-losses';
|
|
4662
|
+
if (signal === 'conflict' ||
|
|
4663
|
+
signal === 'conflicts' ||
|
|
4664
|
+
signal === 'symbol-conflict' ||
|
|
4665
|
+
signal === 'effect-conflict' ||
|
|
4666
|
+
signal === 'blocked')
|
|
4667
|
+
return 'conflict';
|
|
4668
|
+
if (signal === 'no-op' ||
|
|
4669
|
+
signal === 'noop' ||
|
|
4670
|
+
signal === 'not-applicable' ||
|
|
4671
|
+
signal === 'no-semantic-edit-script' ||
|
|
4672
|
+
signal === 'evidence-only')
|
|
4673
|
+
return 'no-op';
|
|
4674
|
+
if (signal === 'stale' ||
|
|
4675
|
+
signal === 'rerun' ||
|
|
4676
|
+
signal === 'stale-against-head' ||
|
|
4677
|
+
signal === 'stale-source-hash')
|
|
4678
|
+
return 'stale';
|
|
4679
|
+
if (signal === 'missing-sidecar' ||
|
|
4680
|
+
signal === 'empty-sidecar')
|
|
4681
|
+
return 'missing-sidecar';
|
|
4682
|
+
if (signal === 'coordinator-review' ||
|
|
4683
|
+
signal === 'needs-coordinator-review' ||
|
|
4684
|
+
signal === 'needs-coordinator-port' ||
|
|
4685
|
+
signal === 'needs-human-port' ||
|
|
4686
|
+
signal === 'needs-human-review' ||
|
|
4687
|
+
signal === 'review' ||
|
|
4688
|
+
signal === 'review-required' ||
|
|
4689
|
+
signal === 'needs-review' ||
|
|
4690
|
+
signal === 'needs-port')
|
|
4691
|
+
return 'coordinator-review';
|
|
4692
|
+
if (signal === 'tests-missing' ||
|
|
4693
|
+
signal === 'missing-tests')
|
|
4694
|
+
return 'tests-missing';
|
|
4695
|
+
return undefined;
|
|
4696
|
+
}
|
|
4697
|
+
function setRecordMinimum(record, key, value) {
|
|
4698
|
+
if (value > (record[key] ?? 0))
|
|
4699
|
+
record[key] = value;
|
|
4700
|
+
}
|
|
4701
|
+
function semanticGateLabel(value) {
|
|
4702
|
+
const status = normalized(value);
|
|
4703
|
+
if (status === 'pass')
|
|
4704
|
+
return 'Pass';
|
|
4705
|
+
if (status === 'review')
|
|
4706
|
+
return 'Review';
|
|
4707
|
+
if (status === 'blocked')
|
|
4708
|
+
return 'Blocked';
|
|
4709
|
+
return 'Unknown';
|
|
4710
|
+
}
|
|
4711
|
+
function firstNumber(...values) {
|
|
4712
|
+
for (const value of values) {
|
|
4713
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
4714
|
+
return value;
|
|
4715
|
+
}
|
|
4716
|
+
return 0;
|
|
4717
|
+
}
|
|
4033
4718
|
function semanticSuccessRows(value) {
|
|
4034
4719
|
const input = recordValue(value);
|
|
4035
4720
|
const imports = recordValue(input.import);
|
|
@@ -4082,6 +4767,15 @@ function admissionTone(value) {
|
|
|
4082
4767
|
function recordValue(value) {
|
|
4083
4768
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
4084
4769
|
}
|
|
4770
|
+
function numberRecordValue(value) {
|
|
4771
|
+
const out = {};
|
|
4772
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
4773
|
+
const count = numberValue(entry);
|
|
4774
|
+
if (count > 0)
|
|
4775
|
+
out[key] = count;
|
|
4776
|
+
}
|
|
4777
|
+
return out;
|
|
4778
|
+
}
|
|
4085
4779
|
function arrayRecords(value) {
|
|
4086
4780
|
return Array.isArray(value) ? value.map(recordValue).filter((entry) => Object.keys(entry).length > 0) : [];
|
|
4087
4781
|
}
|
|
@@ -4091,6 +4785,9 @@ function stringArray(value) {
|
|
|
4091
4785
|
function normalized(value) {
|
|
4092
4786
|
return textValue(value, '').toLowerCase();
|
|
4093
4787
|
}
|
|
4788
|
+
function normalizedStatusKey(value) {
|
|
4789
|
+
return textValue(value, '').trim().replace(/[\s_]+/g, '-').toLowerCase();
|
|
4790
|
+
}
|
|
4094
4791
|
function formatTime(value) {
|
|
4095
4792
|
const number = Number(value);
|
|
4096
4793
|
if (!Number.isFinite(number) || number <= 0)
|