@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/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 = evidenceCount ? 'Evidence only' : worker.status === 'active' ? 'Files pending' : 'No source files';
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: details?.error ? `Diff unavailable: ${details.error}` : 'No patch diff was available in the collected evidence.' }), _jsx(ArtifactPathList, { paths: changedPaths })] });
556
- return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("p", { className: "empty tight", children: job.boardKind === 'backlog' ? 'No files yet. This item has not produced an implementation patch.' : 'No changed files reported.' })] });
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
- return `${taskTitle(job)} is in ${taskBoardColumnTitle(taskBoardColumnId(job)).toLowerCase()} with ${changed} changed paths and ${evidence} evidence artifacts.${reasonText}`;
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 'evidence only';
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: numberValue(imports.expectedCount),
4020
- satisfied: numberValue(imports.expectedSatisfiedCount),
4021
- candidates: numberValue(imports.candidateCount),
4022
- autoMerge: numberValue(script.autoMergeCandidateCount),
4023
- acceptedClean: numberValue(replay.acceptedCleanCount),
4024
- conflicts: numberValue(replay.conflictCount)
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)