agentic-orchestrator 0.1.25 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/agentic/orchestrator/defaults/policy.defaults.yaml +1 -0
  2. package/agentic/orchestrator/policy.yaml +1 -0
  3. package/agentic/orchestrator/schemas/policy.schema.json +4 -0
  4. package/agentic/orchestrator/schemas/policy.user.schema.json +4 -0
  5. package/apps/control-plane/src/application/services/activity-monitor-service.ts +42 -0
  6. package/apps/control-plane/src/application/services/collision-queue-service.ts +48 -0
  7. package/apps/control-plane/src/application/services/cost-tracking-service.ts +80 -0
  8. package/apps/control-plane/src/application/services/feature-deletion-service.ts +57 -0
  9. package/apps/control-plane/src/application/services/feature-lifecycle-service.ts +65 -0
  10. package/apps/control-plane/src/application/services/feature-state-service.ts +87 -0
  11. package/apps/control-plane/src/application/services/gate-selection-service.ts +28 -0
  12. package/apps/control-plane/src/application/services/gate-service.ts +148 -0
  13. package/apps/control-plane/src/application/services/issue-tracker-service.ts +28 -0
  14. package/apps/control-plane/src/application/services/lock-service.ts +88 -0
  15. package/apps/control-plane/src/application/services/merge-service.ts +63 -0
  16. package/apps/control-plane/src/application/services/notifier-service.ts +70 -0
  17. package/apps/control-plane/src/application/services/patch-service.ts +95 -0
  18. package/apps/control-plane/src/application/services/performance-analytics-service.ts +90 -0
  19. package/apps/control-plane/src/application/services/plan-service.ts +184 -0
  20. package/apps/control-plane/src/application/services/pr-monitor-service.ts +58 -0
  21. package/apps/control-plane/src/application/services/qa-index-service.ts +68 -0
  22. package/apps/control-plane/src/application/services/reactions-service.ts +77 -0
  23. package/apps/control-plane/src/application/services/reporting-service.ts +79 -0
  24. package/apps/control-plane/src/application/services/run-lease-service.ts +79 -0
  25. package/apps/control-plane/src/application/tools/tool-router.ts +76 -0
  26. package/apps/control-plane/src/mcp/tool-runtime.ts +4 -0
  27. package/apps/control-plane/src/providers/providers.ts +96 -0
  28. package/apps/control-plane/src/providers/worker-provider-factory.ts +4 -0
  29. package/apps/control-plane/src/supervisor/build-wave-executor.ts +4 -0
  30. package/apps/control-plane/src/supervisor/planning-wave-executor.ts +4 -0
  31. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +4 -0
  32. package/apps/control-plane/src/supervisor/run-coordinator.ts +4 -0
  33. package/apps/control-plane/src/supervisor/runtime.ts +4 -0
  34. package/apps/control-plane/src/supervisor/session-orchestrator.ts +4 -0
  35. package/apps/control-plane/test/dashboard-api.integration.spec.ts +310 -1
  36. package/apps/control-plane/test/dashboard-client.spec.ts +136 -1
  37. package/apps/control-plane/test/dashboard-live-feed.spec.ts +153 -0
  38. package/apps/control-plane/test/dashboard-ui-utils.spec.ts +132 -0
  39. package/apps/control-plane/test/mcp.spec.ts +96 -0
  40. package/apps/control-plane/test/tool-runtime.spec.ts +68 -0
  41. package/config/agentic/orchestrator/policy.yaml +1 -0
  42. package/dist/apps/control-plane/application/services/activity-monitor-service.d.ts +42 -0
  43. package/dist/apps/control-plane/application/services/activity-monitor-service.js +42 -0
  44. package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -1
  45. package/dist/apps/control-plane/application/services/collision-queue-service.d.ts +48 -0
  46. package/dist/apps/control-plane/application/services/collision-queue-service.js +48 -0
  47. package/dist/apps/control-plane/application/services/collision-queue-service.js.map +1 -1
  48. package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +79 -0
  49. package/dist/apps/control-plane/application/services/cost-tracking-service.js +76 -0
  50. package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -1
  51. package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +57 -0
  52. package/dist/apps/control-plane/application/services/feature-deletion-service.js +57 -0
  53. package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
  54. package/dist/apps/control-plane/application/services/feature-lifecycle-service.d.ts +64 -0
  55. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js +61 -0
  56. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js.map +1 -1
  57. package/dist/apps/control-plane/application/services/feature-state-service.d.ts +86 -0
  58. package/dist/apps/control-plane/application/services/feature-state-service.js +83 -0
  59. package/dist/apps/control-plane/application/services/feature-state-service.js.map +1 -1
  60. package/dist/apps/control-plane/application/services/gate-selection-service.d.ts +28 -0
  61. package/dist/apps/control-plane/application/services/gate-selection-service.js +28 -0
  62. package/dist/apps/control-plane/application/services/gate-selection-service.js.map +1 -1
  63. package/dist/apps/control-plane/application/services/gate-service.d.ts +148 -0
  64. package/dist/apps/control-plane/application/services/gate-service.js +120 -0
  65. package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
  66. package/dist/apps/control-plane/application/services/issue-tracker-service.d.ts +28 -0
  67. package/dist/apps/control-plane/application/services/issue-tracker-service.js +28 -0
  68. package/dist/apps/control-plane/application/services/issue-tracker-service.js.map +1 -1
  69. package/dist/apps/control-plane/application/services/lock-service.d.ts +88 -0
  70. package/dist/apps/control-plane/application/services/lock-service.js +64 -0
  71. package/dist/apps/control-plane/application/services/lock-service.js.map +1 -1
  72. package/dist/apps/control-plane/application/services/merge-service.d.ts +62 -0
  73. package/dist/apps/control-plane/application/services/merge-service.js +59 -0
  74. package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
  75. package/dist/apps/control-plane/application/services/notifier-service.d.ts +70 -0
  76. package/dist/apps/control-plane/application/services/notifier-service.js +70 -0
  77. package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -1
  78. package/dist/apps/control-plane/application/services/patch-service.d.ts +94 -0
  79. package/dist/apps/control-plane/application/services/patch-service.js +91 -0
  80. package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
  81. package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +89 -0
  82. package/dist/apps/control-plane/application/services/performance-analytics-service.js +86 -0
  83. package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -1
  84. package/dist/apps/control-plane/application/services/plan-service.d.ts +287 -0
  85. package/dist/apps/control-plane/application/services/plan-service.js +207 -0
  86. package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
  87. package/dist/apps/control-plane/application/services/pr-monitor-service.d.ts +58 -0
  88. package/dist/apps/control-plane/application/services/pr-monitor-service.js +58 -0
  89. package/dist/apps/control-plane/application/services/pr-monitor-service.js.map +1 -1
  90. package/dist/apps/control-plane/application/services/qa-index-service.d.ts +67 -0
  91. package/dist/apps/control-plane/application/services/qa-index-service.js +64 -0
  92. package/dist/apps/control-plane/application/services/qa-index-service.js.map +1 -1
  93. package/dist/apps/control-plane/application/services/reactions-service.d.ts +76 -0
  94. package/dist/apps/control-plane/application/services/reactions-service.js +73 -0
  95. package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -1
  96. package/dist/apps/control-plane/application/services/reporting-service.d.ts +78 -0
  97. package/dist/apps/control-plane/application/services/reporting-service.js +75 -0
  98. package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
  99. package/dist/apps/control-plane/application/services/run-lease-service.d.ts +79 -0
  100. package/dist/apps/control-plane/application/services/run-lease-service.js +79 -0
  101. package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
  102. package/dist/apps/control-plane/application/tools/tool-router.d.ts +76 -0
  103. package/dist/apps/control-plane/application/tools/tool-router.js +62 -0
  104. package/dist/apps/control-plane/application/tools/tool-router.js.map +1 -1
  105. package/dist/apps/control-plane/mcp/tool-runtime.d.ts +3 -0
  106. package/dist/apps/control-plane/mcp/tool-runtime.js +3 -0
  107. package/dist/apps/control-plane/mcp/tool-runtime.js.map +1 -1
  108. package/dist/apps/control-plane/providers/providers.d.ts +88 -0
  109. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  110. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +3 -0
  111. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
  112. package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +3 -0
  113. package/dist/apps/control-plane/supervisor/build-wave-executor.js +3 -0
  114. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  115. package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +3 -0
  116. package/dist/apps/control-plane/supervisor/planning-wave-executor.js +3 -0
  117. package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
  118. package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +3 -0
  119. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +3 -0
  120. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  121. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
  122. package/dist/apps/control-plane/supervisor/run-coordinator.js +3 -0
  123. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  124. package/dist/apps/control-plane/supervisor/runtime.d.ts +3 -0
  125. package/dist/apps/control-plane/supervisor/runtime.js +3 -0
  126. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  127. package/dist/apps/control-plane/supervisor/session-orchestrator.d.ts +3 -0
  128. package/dist/apps/control-plane/supervisor/session-orchestrator.js +3 -0
  129. package/dist/apps/control-plane/supervisor/session-orchestrator.js.map +1 -1
  130. package/package.json +1 -1
  131. package/packages/web-dashboard/src/app/analytics/page.tsx +451 -0
  132. package/packages/web-dashboard/src/app/api/analytics/route.ts +62 -0
  133. package/packages/web-dashboard/src/app/api/collisions/route.ts +63 -0
  134. package/packages/web-dashboard/src/app/api/features/[id]/cost/route.ts +29 -0
  135. package/packages/web-dashboard/src/app/api/features/[id]/review-brief/route.ts +31 -0
  136. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +5 -8
  137. package/packages/web-dashboard/src/app/api/features/[id]/test-index/route.ts +31 -0
  138. package/packages/web-dashboard/src/app/api/flaky/route.ts +20 -0
  139. package/packages/web-dashboard/src/app/api/policy/budget/route.ts +31 -0
  140. package/packages/web-dashboard/src/app/api/run/route.ts +162 -0
  141. package/packages/web-dashboard/src/app/api/status/route.ts +2 -5
  142. package/packages/web-dashboard/src/app/feature/[id]/page.tsx +112 -0
  143. package/packages/web-dashboard/src/app/page.tsx +379 -77
  144. package/packages/web-dashboard/src/components/agent-pipeline-stepper.tsx +100 -0
  145. package/packages/web-dashboard/src/components/dependency-chains.tsx +58 -0
  146. package/packages/web-dashboard/src/components/detail-panel.tsx +215 -5
  147. package/packages/web-dashboard/src/components/feature-card.tsx +5 -0
  148. package/packages/web-dashboard/src/components/feature-cost-panel.tsx +66 -0
  149. package/packages/web-dashboard/src/components/feature-list-view.tsx +336 -0
  150. package/packages/web-dashboard/src/components/gate-step-drilldown.tsx +92 -0
  151. package/packages/web-dashboard/src/components/kanban-board.tsx +3 -0
  152. package/packages/web-dashboard/src/components/live-event-feed.tsx +60 -0
  153. package/packages/web-dashboard/src/components/lock-resource-map.tsx +73 -0
  154. package/packages/web-dashboard/src/components/plan-revision-timeline.tsx +55 -0
  155. package/packages/web-dashboard/src/components/plan-risk-annotations.tsx +63 -0
  156. package/packages/web-dashboard/src/components/plan-scope-tree.tsx +285 -0
  157. package/packages/web-dashboard/src/components/qa-coverage-map.tsx +174 -0
  158. package/packages/web-dashboard/src/components/quick-launch-panel.tsx +70 -0
  159. package/packages/web-dashboard/src/components/review-brief-panel.tsx +75 -0
  160. package/packages/web-dashboard/src/components/run-health-panel.tsx +85 -0
  161. package/packages/web-dashboard/src/components/summary-bar.tsx +59 -2
  162. package/packages/web-dashboard/src/components/verification-signals.tsx +167 -0
  163. package/packages/web-dashboard/src/lib/analytics-utils.ts +3 -0
  164. package/packages/web-dashboard/src/lib/aop-client.ts +510 -11
  165. package/packages/web-dashboard/src/lib/api-envelope.ts +48 -0
  166. package/packages/web-dashboard/src/lib/authz-adapter.ts +33 -0
  167. package/packages/web-dashboard/src/lib/dashboard-utils.ts +304 -0
  168. package/packages/web-dashboard/src/lib/live-feed.ts +218 -0
  169. package/packages/web-dashboard/src/lib/policy-reader.ts +22 -0
  170. package/packages/web-dashboard/src/lib/types.ts +114 -0
  171. package/packages/web-dashboard/src/styles/dashboard.module.css +341 -1
  172. package/spec-files/{outstanding → completed}/agentic_orchestrator_dashboard_advanced_ux_spec.md +1 -1
  173. package/spec-files/outstanding/agentic_orchestrator_evidence_integrity_doctor_spec.md +561 -0
  174. package/spec-files/outstanding/agentic_orchestrator_kernel_simplification_spec.md +834 -0
  175. package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +12 -5
  176. package/spec-files/outstanding/agentic_orchestrator_observability_replay_spec.md +442 -0
  177. package/spec-files/progress.md +174 -1
@@ -0,0 +1,63 @@
1
+ import styles from '@/styles/dashboard.module.css';
2
+
3
+ interface PlanRiskAnnotationsProps {
4
+ plan: Record<string, unknown> | null;
5
+ expandedByDefault?: boolean;
6
+ }
7
+
8
+ interface ParsedRisk {
9
+ severity: 'high' | 'medium' | 'low';
10
+ message: string;
11
+ }
12
+
13
+ function parseRisks(raw: unknown): ParsedRisk[] {
14
+ if (!Array.isArray(raw)) {
15
+ return [];
16
+ }
17
+ return raw
18
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
19
+ .filter((item) => item.length > 0)
20
+ .map((item) => {
21
+ const match = item.match(/^\[(high|medium|low)\]\s*(.+)$/i);
22
+ if (!match) {
23
+ return { severity: 'medium', message: item } as ParsedRisk;
24
+ }
25
+ return {
26
+ severity: match[1].toLowerCase() as ParsedRisk['severity'],
27
+ message: match[2].trim(),
28
+ };
29
+ });
30
+ }
31
+
32
+ function severityClass(severity: ParsedRisk['severity']): string {
33
+ if (severity === 'high') {
34
+ return styles.badgeFail;
35
+ }
36
+ if (severity === 'low') {
37
+ return styles.badgePass;
38
+ }
39
+ return styles.badgePending;
40
+ }
41
+
42
+ export function PlanRiskAnnotations({ plan, expandedByDefault = false }: PlanRiskAnnotationsProps) {
43
+ const risks = parseRisks(plan?.risk);
44
+ if (risks.length === 0) {
45
+ return null;
46
+ }
47
+
48
+ return (
49
+ <details className={styles.section} open={expandedByDefault}>
50
+ <summary className={styles.sectionSummary}>Plan Risks</summary>
51
+ <ul className={styles.planList}>
52
+ {risks.map((risk, index) => (
53
+ <li key={`${risk.message}-${index}`} className={styles.planListItem}>
54
+ <span className={`${styles.badge} ${severityClass(risk.severity)}`}>
55
+ {risk.severity}
56
+ </span>{' '}
57
+ {risk.message}
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ </details>
62
+ );
63
+ }
@@ -0,0 +1,285 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import styles from '@/styles/dashboard.module.css';
5
+
6
+ interface PlanScopeTreeProps {
7
+ plan: Record<string, unknown> | null;
8
+ }
9
+
10
+ type FileOperation = 'Created' | 'Modified' | 'Deleted';
11
+
12
+ interface FileNode {
13
+ name: string;
14
+ path: string;
15
+ children: Map<string, FileNode>;
16
+ operations: Set<FileOperation>;
17
+ }
18
+
19
+ interface FileEntry {
20
+ path: string;
21
+ op: FileOperation;
22
+ }
23
+
24
+ const LARGE_TREE_NODE_THRESHOLD = 200;
25
+ const TREE_ENTRY_CHUNK_SIZE = 120;
26
+
27
+ function parseStringArray(value: unknown): string[] {
28
+ if (!Array.isArray(value)) {
29
+ return [];
30
+ }
31
+ return value.filter((item): item is string => typeof item === 'string');
32
+ }
33
+
34
+ function collectEntries(plan: Record<string, unknown>): FileEntry[] {
35
+ const filesRaw = plan.files;
36
+ const fileEntries: FileEntry[] = [];
37
+ if (Array.isArray(filesRaw)) {
38
+ for (const item of filesRaw) {
39
+ if (typeof item === 'string') {
40
+ fileEntries.push({ path: item, op: 'Modified' });
41
+ }
42
+ }
43
+ return fileEntries;
44
+ }
45
+
46
+ if (!filesRaw || typeof filesRaw !== 'object') {
47
+ return fileEntries;
48
+ }
49
+
50
+ const files = filesRaw as Record<string, unknown>;
51
+ for (const item of parseStringArray(files.create)) {
52
+ fileEntries.push({ path: item, op: 'Created' });
53
+ }
54
+ for (const item of parseStringArray(files.modify)) {
55
+ fileEntries.push({ path: item, op: 'Modified' });
56
+ }
57
+ for (const item of parseStringArray(files.delete)) {
58
+ fileEntries.push({ path: item, op: 'Deleted' });
59
+ }
60
+ return fileEntries;
61
+ }
62
+
63
+ function createRoot(): FileNode {
64
+ return {
65
+ name: '/',
66
+ path: '',
67
+ children: new Map(),
68
+ operations: new Set(),
69
+ };
70
+ }
71
+
72
+ function addEntry(root: FileNode, entry: FileEntry): void {
73
+ const parts = entry.path.split('/').filter((part) => part.length > 0);
74
+ if (parts.length === 0) {
75
+ return;
76
+ }
77
+ let node = root;
78
+ for (const part of parts) {
79
+ const nextPath = node.path.length > 0 ? `${node.path}/${part}` : part;
80
+ const existing = node.children.get(part);
81
+ if (existing) {
82
+ node = existing;
83
+ continue;
84
+ }
85
+ const created: FileNode = {
86
+ name: part,
87
+ path: nextPath,
88
+ children: new Map(),
89
+ operations: new Set(),
90
+ };
91
+ node.children.set(part, created);
92
+ node = created;
93
+ }
94
+ node.operations.add(entry.op);
95
+ }
96
+
97
+ function buildTree(entries: FileEntry[]): FileNode {
98
+ const root = createRoot();
99
+ for (const entry of entries) {
100
+ addEntry(root, entry);
101
+ }
102
+ return root;
103
+ }
104
+
105
+ function countTreeNodes(node: FileNode): number {
106
+ let count = 1;
107
+ for (const child of node.children.values()) {
108
+ count += countTreeNodes(child);
109
+ }
110
+ return count;
111
+ }
112
+
113
+ function operationClass(op: FileOperation): string {
114
+ if (op === 'Created') {
115
+ return styles.badgePass;
116
+ }
117
+ if (op === 'Deleted') {
118
+ return styles.badgeFail;
119
+ }
120
+ return styles.badgePending;
121
+ }
122
+
123
+ function operationLabel(op: FileOperation): string {
124
+ if (op === 'Created') {
125
+ return '+ Created';
126
+ }
127
+ if (op === 'Deleted') {
128
+ return '- Deleted';
129
+ }
130
+ return '~ Modified';
131
+ }
132
+
133
+ function renderNode(node: FileNode, depth: number): JSX.Element {
134
+ const children = [...node.children.values()].sort((left, right) =>
135
+ left.name.localeCompare(right.name),
136
+ );
137
+ const hasChildren = children.length > 0;
138
+ const badges = [...node.operations].sort((left, right) => left.localeCompare(right));
139
+
140
+ if (!hasChildren) {
141
+ return (
142
+ <li key={node.path} className={styles.planTreeLeaf}>
143
+ <span>{node.name}</span>
144
+ <span className={styles.badgeRow}>
145
+ {badges.map((op) => (
146
+ <span key={`${node.path}-${op}`} className={`${styles.badge} ${operationClass(op)}`}>
147
+ {operationLabel(op)}
148
+ </span>
149
+ ))}
150
+ </span>
151
+ </li>
152
+ );
153
+ }
154
+
155
+ return (
156
+ <li key={node.path} className={styles.planTreeBranch}>
157
+ <details open={depth < 2}>
158
+ <summary className={styles.sectionSummary}>{node.name}</summary>
159
+ <ul className={styles.planTreeList}>
160
+ {children.map((child) => renderNode(child, depth + 1))}
161
+ </ul>
162
+ </details>
163
+ </li>
164
+ );
165
+ }
166
+
167
+ export function PlanScopeTree({ plan }: PlanScopeTreeProps) {
168
+ const [chunkIndex, setChunkIndex] = useState(0);
169
+
170
+ const entries = useMemo(() => {
171
+ if (!plan) {
172
+ return [];
173
+ }
174
+ return collectEntries(plan);
175
+ }, [plan]);
176
+
177
+ const allowedAreas = useMemo(() => {
178
+ if (!plan) {
179
+ return [];
180
+ }
181
+ return parseStringArray(plan.allowed_areas);
182
+ }, [plan]);
183
+
184
+ const forbiddenAreas = useMemo(() => {
185
+ if (!plan) {
186
+ return [];
187
+ }
188
+ return parseStringArray(plan.forbidden_areas);
189
+ }, [plan]);
190
+
191
+ useEffect(() => {
192
+ setChunkIndex(0);
193
+ }, [plan, entries.length]);
194
+
195
+ const fullTreeNodeCount = useMemo(() => {
196
+ if (entries.length === 0) {
197
+ return 0;
198
+ }
199
+ const fullTree = buildTree(entries);
200
+ return Math.max(0, countTreeNodes(fullTree) - 1);
201
+ }, [entries]);
202
+
203
+ const chunkingEnabled = fullTreeNodeCount > LARGE_TREE_NODE_THRESHOLD;
204
+ const chunkCount = chunkingEnabled
205
+ ? Math.max(1, Math.ceil(entries.length / TREE_ENTRY_CHUNK_SIZE))
206
+ : 1;
207
+ const currentChunk = Math.min(chunkIndex, chunkCount - 1);
208
+ const chunkStart = chunkingEnabled ? currentChunk * TREE_ENTRY_CHUNK_SIZE : 0;
209
+ const chunkEnd = chunkingEnabled
210
+ ? Math.min(entries.length, chunkStart + TREE_ENTRY_CHUNK_SIZE)
211
+ : entries.length;
212
+ const visibleEntries = chunkingEnabled ? entries.slice(chunkStart, chunkEnd) : entries;
213
+
214
+ const rootChildren = useMemo(() => {
215
+ const root = buildTree(visibleEntries);
216
+ return [...root.children.values()].sort((left, right) => left.name.localeCompare(right.name));
217
+ }, [visibleEntries]);
218
+
219
+ if (!plan || entries.length === 0) {
220
+ return null;
221
+ }
222
+
223
+ return (
224
+ <details className={styles.section} open>
225
+ <summary className={styles.sectionSummary}>Plan Scope</summary>
226
+ <div className={styles.planPolicyGroup}>
227
+ <span className={styles.metaText}>Allowed Areas</span>
228
+ <div className={styles.badgeRow}>
229
+ {allowedAreas.length > 0 ? (
230
+ allowedAreas.map((area) => (
231
+ <span key={`allow-${area}`} className={`${styles.badge} ${styles.badgePass}`}>
232
+ Allowed {area}
233
+ </span>
234
+ ))
235
+ ) : (
236
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>None</span>
237
+ )}
238
+ </div>
239
+ </div>
240
+ <div className={styles.planPolicyGroup}>
241
+ <span className={styles.metaText}>Forbidden Areas</span>
242
+ <div className={styles.badgeRow}>
243
+ {forbiddenAreas.length > 0 ? (
244
+ forbiddenAreas.map((area) => (
245
+ <span key={`deny-${area}`} className={`${styles.badge} ${styles.badgeFail}`}>
246
+ Forbidden {area}
247
+ </span>
248
+ ))
249
+ ) : (
250
+ <span className={`${styles.badge} ${styles.badgeNeutral}`}>None</span>
251
+ )}
252
+ </div>
253
+ </div>
254
+ {chunkingEnabled ? (
255
+ <div className={styles.planTreePager} role="group" aria-label="Plan scope pagination">
256
+ <span className={styles.metaText}>
257
+ Large scope mode: showing files {chunkStart + 1}-{chunkEnd} of {entries.length}.
258
+ </span>
259
+ <div className={styles.inlineActions}>
260
+ <button
261
+ type="button"
262
+ className={styles.button}
263
+ onClick={() => setChunkIndex((current) => Math.max(0, current - 1))}
264
+ disabled={currentChunk === 0}
265
+ >
266
+ Previous
267
+ </button>
268
+ <span className={styles.metaText}>
269
+ Page {currentChunk + 1} of {chunkCount}
270
+ </span>
271
+ <button
272
+ type="button"
273
+ className={styles.button}
274
+ onClick={() => setChunkIndex((current) => Math.min(chunkCount - 1, current + 1))}
275
+ disabled={currentChunk >= chunkCount - 1}
276
+ >
277
+ Next
278
+ </button>
279
+ </div>
280
+ </div>
281
+ ) : null}
282
+ <ul className={styles.planTreeList}>{rootChildren.map((node) => renderNode(node, 0))}</ul>
283
+ </details>
284
+ );
285
+ }
@@ -0,0 +1,174 @@
1
+ import { useMemo, useState } from 'react';
2
+ import type { QaTestIndex } from '@/lib/types.js';
3
+ import styles from '@/styles/dashboard.module.css';
4
+
5
+ interface QaCoverageMapProps {
6
+ qaIndex: QaTestIndex | null | undefined;
7
+ flakySuspects?: string[] | null;
8
+ }
9
+
10
+ type StatusFilter = 'all' | 'failed' | 'pending_running' | 'waived' | 'passed';
11
+
12
+ function statusClass(status: string): string {
13
+ if (status === 'passed') {
14
+ return styles.badgePass;
15
+ }
16
+ if (status === 'failed') {
17
+ return styles.badgeFail;
18
+ }
19
+ if (status === 'waived') {
20
+ return styles.badgeNeutral;
21
+ }
22
+ return styles.badgePending;
23
+ }
24
+
25
+ function statusText(status: string): string {
26
+ if (status === 'passed') {
27
+ return '[PASS] passed';
28
+ }
29
+ if (status === 'failed') {
30
+ return '[FAIL] failed';
31
+ }
32
+ if (status === 'waived') {
33
+ return '[WAIVED] waived';
34
+ }
35
+ if (status === 'running') {
36
+ return '[RUNNING] running';
37
+ }
38
+ return '[PENDING] pending';
39
+ }
40
+
41
+ export function QaCoverageMap({ qaIndex, flakySuspects }: QaCoverageMapProps) {
42
+ const [filter, setFilter] = useState<StatusFilter>('all');
43
+ const items = qaIndex?.items ?? [];
44
+ const flakySet = useMemo(
45
+ () => new Set((flakySuspects ?? []).map((item) => item.trim())),
46
+ [flakySuspects],
47
+ );
48
+
49
+ const filteredItems = useMemo(() => {
50
+ if (filter === 'all') {
51
+ return items;
52
+ }
53
+ if (filter === 'failed') {
54
+ return items.filter((item) => item.status === 'failed');
55
+ }
56
+ if (filter === 'pending_running') {
57
+ return items.filter((item) => item.status === 'pending' || item.status === 'running');
58
+ }
59
+ if (filter === 'waived') {
60
+ return items.filter((item) => item.status === 'waived');
61
+ }
62
+ return items.filter((item) => item.status === 'passed');
63
+ }, [items, filter]);
64
+
65
+ const flakyOverlap = useMemo(() => {
66
+ if (flakySet.size === 0) {
67
+ return [];
68
+ }
69
+ const overlaps = new Set<string>();
70
+ for (const item of items) {
71
+ if (item.status !== 'failed') {
72
+ continue;
73
+ }
74
+ for (const testName of item.required_tests) {
75
+ if (flakySet.has(testName)) {
76
+ overlaps.add(testName);
77
+ }
78
+ }
79
+ }
80
+ return Array.from(overlaps).sort((left, right) => left.localeCompare(right));
81
+ }, [items, flakySet]);
82
+
83
+ if (!qaIndex) {
84
+ return (
85
+ <div className={styles.section}>
86
+ <h3 className={styles.sectionTitle}>QA Coverage Map</h3>
87
+ <p className={styles.metaText}>QA index artifact missing.</p>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <details className={styles.section} open>
94
+ <summary className={styles.sectionSummary}>QA Coverage Map</summary>
95
+ <div className={styles.badgeRow}>
96
+ <button
97
+ type="button"
98
+ className={`${styles.filterToggle} ${filter === 'all' ? styles.filterToggleOn : ''}`}
99
+ onClick={() => setFilter('all')}
100
+ >
101
+ All
102
+ </button>
103
+ <button
104
+ type="button"
105
+ className={`${styles.filterToggle} ${filter === 'failed' ? styles.filterToggleOn : ''}`}
106
+ onClick={() => setFilter('failed')}
107
+ >
108
+ failed
109
+ </button>
110
+ <button
111
+ type="button"
112
+ className={`${styles.filterToggle} ${filter === 'pending_running' ? styles.filterToggleOn : ''}`}
113
+ onClick={() => setFilter('pending_running')}
114
+ >
115
+ pending/running
116
+ </button>
117
+ <button
118
+ type="button"
119
+ className={`${styles.filterToggle} ${filter === 'waived' ? styles.filterToggleOn : ''}`}
120
+ onClick={() => setFilter('waived')}
121
+ >
122
+ waived
123
+ </button>
124
+ <button
125
+ type="button"
126
+ className={`${styles.filterToggle} ${filter === 'passed' ? styles.filterToggleOn : ''}`}
127
+ onClick={() => setFilter('passed')}
128
+ >
129
+ passed
130
+ </button>
131
+ </div>
132
+ {flakyOverlap.length > 0 ? (
133
+ <div className={styles.banner}>
134
+ Probabilistic flaky signal: failing required tests overlap flaky suspects (
135
+ {flakyOverlap.join(', ')}).
136
+ </div>
137
+ ) : null}
138
+ <div className={styles.tableWrapper}>
139
+ <table className={styles.table}>
140
+ <thead>
141
+ <tr>
142
+ <th>File</th>
143
+ <th>Status</th>
144
+ <th>Required tests</th>
145
+ <th>Last run</th>
146
+ </tr>
147
+ </thead>
148
+ <tbody>
149
+ {filteredItems.length === 0 ? (
150
+ <tr>
151
+ <td colSpan={4} className={styles.metaText}>
152
+ No rows for selected filter.
153
+ </td>
154
+ </tr>
155
+ ) : (
156
+ filteredItems.map((item) => (
157
+ <tr key={item.path}>
158
+ <td>{item.path}</td>
159
+ <td>
160
+ <span className={`${styles.badge} ${statusClass(item.status)}`}>
161
+ {statusText(item.status)}
162
+ </span>
163
+ </td>
164
+ <td>{item.required_tests.length}</td>
165
+ <td>{item.last_run_at ?? '-'}</td>
166
+ </tr>
167
+ ))
168
+ )}
169
+ </tbody>
170
+ </table>
171
+ </div>
172
+ </details>
173
+ );
174
+ }
@@ -0,0 +1,70 @@
1
+ import styles from '@/styles/dashboard.module.css';
2
+
3
+ interface QuickLaunchPanelProps {
4
+ visible: boolean;
5
+ featureId: string;
6
+ goal: string;
7
+ submitting: boolean;
8
+ disabledReason: string | null;
9
+ onFeatureIdChange: (value: string) => void;
10
+ onGoalChange: (value: string) => void;
11
+ onRequestLaunch: () => void;
12
+ }
13
+
14
+ export function QuickLaunchPanel({
15
+ visible,
16
+ featureId,
17
+ goal,
18
+ submitting,
19
+ disabledReason,
20
+ onFeatureIdChange,
21
+ onGoalChange,
22
+ onRequestLaunch,
23
+ }: QuickLaunchPanelProps) {
24
+ if (!visible) {
25
+ return null;
26
+ }
27
+
28
+ const launchDisabled = submitting || featureId.trim().length === 0 || goal.trim().length === 0;
29
+
30
+ return (
31
+ <section className={styles.section}>
32
+ <h2 className={styles.sectionTitle}>Quick Launch</h2>
33
+ <p className={styles.metaText}>
34
+ Create a new feature run with policy validation and explicit confirmation.
35
+ </p>
36
+ {disabledReason ? <p className={styles.metaText}>{disabledReason}</p> : null}
37
+ <label className={styles.label} htmlFor="quick-launch-feature-id">
38
+ Feature ID
39
+ </label>
40
+ <input
41
+ id="quick-launch-feature-id"
42
+ className={styles.input}
43
+ value={featureId}
44
+ onChange={(event) => onFeatureIdChange(event.target.value)}
45
+ placeholder="feature_checkout_flow"
46
+ />
47
+ <label className={styles.label} htmlFor="quick-launch-goal">
48
+ Goal
49
+ </label>
50
+ <textarea
51
+ id="quick-launch-goal"
52
+ className={styles.textarea}
53
+ rows={3}
54
+ value={goal}
55
+ onChange={(event) => onGoalChange(event.target.value)}
56
+ placeholder="Implement ..."
57
+ />
58
+ <div className={styles.inlineActions}>
59
+ <button
60
+ type="button"
61
+ className={`${styles.button} ${styles.buttonSuccess}`}
62
+ disabled={launchDisabled}
63
+ onClick={onRequestLaunch}
64
+ >
65
+ {submitting ? 'Launching...' : 'Launch Feature'}
66
+ </button>
67
+ </div>
68
+ </section>
69
+ );
70
+ }
@@ -0,0 +1,75 @@
1
+ import type { ReviewBrief } from '@/lib/types.js';
2
+ import styles from '@/styles/dashboard.module.css';
3
+
4
+ interface ReviewBriefPanelProps {
5
+ brief: ReviewBrief | null | undefined;
6
+ }
7
+
8
+ function freshnessAge(generatedAt: string): string {
9
+ const parsed = Date.parse(generatedAt);
10
+ if (Number.isNaN(parsed)) {
11
+ return '-';
12
+ }
13
+ const ageMinutes = Math.max(0, Math.floor((Date.now() - parsed) / 60_000));
14
+ if (ageMinutes < 60) {
15
+ return `${ageMinutes}m ago`;
16
+ }
17
+ const ageHours = Math.floor(ageMinutes / 60);
18
+ return `${ageHours}h ago`;
19
+ }
20
+
21
+ export function ReviewBriefPanel({ brief }: ReviewBriefPanelProps) {
22
+ if (!brief) {
23
+ return (
24
+ <div className={styles.section}>
25
+ <h3 className={styles.sectionTitle}>Review Brief</h3>
26
+ <p className={styles.metaText}>Review brief not generated yet.</p>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ return (
32
+ <details className={styles.section} open>
33
+ <summary className={styles.sectionSummary}>Review Brief</summary>
34
+ <p className={styles.metaText}>
35
+ Generated: {brief.generated_at} ({freshnessAge(brief.generated_at)})
36
+ </p>
37
+ <ul className={styles.planList}>
38
+ <li className={styles.planListItem}>
39
+ <strong>Intent:</strong> {brief.intent_summary}
40
+ </li>
41
+ <li className={styles.planListItem}>
42
+ <strong>Scope:</strong> {brief.scope_summary}
43
+ </li>
44
+ <li className={styles.planListItem}>
45
+ <strong>Contract risk:</strong> {brief.contract_risk_summary}
46
+ </li>
47
+ </ul>
48
+ <div className={styles.badgeRow}>
49
+ {Object.entries(brief.gate_matrix).map(([key, value]) => (
50
+ <span key={key} className={`${styles.badge} ${styles.badgeNeutral}`}>
51
+ {key}: {value}
52
+ </span>
53
+ ))}
54
+ </div>
55
+ {brief.unresolved_questions.length > 0 ? (
56
+ <ul className={styles.planList}>
57
+ {brief.unresolved_questions.map((question) => (
58
+ <li key={question} className={styles.planListItem}>
59
+ {question}
60
+ </li>
61
+ ))}
62
+ </ul>
63
+ ) : null}
64
+ {brief.evidence_refs.length > 0 ? (
65
+ <ul className={styles.planList}>
66
+ {brief.evidence_refs.map((evidence) => (
67
+ <li key={evidence} className={styles.planListItem}>
68
+ {evidence}
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ ) : null}
73
+ </details>
74
+ );
75
+ }