helixevo 0.3.0 → 0.4.0
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/CHANGELOG.md +35 -0
- package/README.md +33 -8
- package/dashboard/app/api/governance/route.ts +49 -0
- package/dashboard/app/api/run/route.ts +31 -4
- package/dashboard/app/api/topology-apply/route.ts +80 -0
- package/dashboard/app/api/topology-review/route.ts +51 -0
- package/dashboard/app/coevolution/client.tsx +560 -0
- package/dashboard/app/coevolution/page.tsx +9 -0
- package/dashboard/app/commands/page.tsx +30 -7
- package/dashboard/app/guide/page.tsx +20 -4
- package/dashboard/app/network/client.tsx +76 -2
- package/dashboard/app/page.tsx +46 -0
- package/dashboard/app/projects/client.tsx +83 -10
- package/dashboard/app/projects/page.tsx +8 -27
- package/dashboard/app/research/client.tsx +48 -8
- package/dashboard/app/topology/client.tsx +575 -0
- package/dashboard/app/topology/page.tsx +10 -0
- package/dashboard/components/sidebar-nav.tsx +2 -0
- package/dashboard/lib/data.ts +1301 -3
- package/dist/cli.js +1981 -124
- package/package.json +2 -2
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { useRef, useState } from 'react'
|
|
5
|
+
import { ConsolePanel } from '@/components/console-panel'
|
|
6
|
+
import { MetricCard } from '@/components/metric-card'
|
|
7
|
+
import { PageHero } from '@/components/page-hero'
|
|
8
|
+
import { SectionFrame } from '@/components/section-frame'
|
|
9
|
+
import type { GovernanceModeName, GovernanceState, TopologyDashboardSummary, TopologyReviewDecisionStatus, TopologyReviewStatus } from '@/lib/data'
|
|
10
|
+
|
|
11
|
+
type RunState = 'idle' | 'running' | 'success' | 'error' | 'stopped'
|
|
12
|
+
|
|
13
|
+
const GOVERNANCE_MODES: GovernanceModeName[] = [
|
|
14
|
+
'balanced',
|
|
15
|
+
'transfer-focused',
|
|
16
|
+
'consolidation-focused',
|
|
17
|
+
'project-critical',
|
|
18
|
+
'exploration',
|
|
19
|
+
'conservative',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
function formatDate(value: string | undefined) {
|
|
23
|
+
if (!value) return '—'
|
|
24
|
+
return new Date(value).toLocaleString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatMode(mode: GovernanceModeName | undefined) {
|
|
28
|
+
if (!mode) return 'auto'
|
|
29
|
+
return mode.split('-').join(' ')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toneForMode(mode: GovernanceModeName): 'blue' | 'green' | 'purple' | 'yellow' | 'neutral' {
|
|
33
|
+
if (mode === 'transfer-focused') return 'purple'
|
|
34
|
+
if (mode === 'project-critical') return 'yellow'
|
|
35
|
+
if (mode === 'exploration') return 'blue'
|
|
36
|
+
if (mode === 'consolidation-focused') return 'green'
|
|
37
|
+
if (mode === 'conservative') return 'neutral'
|
|
38
|
+
return 'blue'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toneForStatus(status: TopologyReviewStatus): 'yellow' | 'green' | 'neutral' | 'purple' {
|
|
42
|
+
if (status === 'accepted') return 'green'
|
|
43
|
+
if (status === 'deferred') return 'purple'
|
|
44
|
+
if (status === 'rejected') return 'neutral'
|
|
45
|
+
return 'yellow'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toneForChange(changeType: string): 'blue' | 'green' | 'purple' | 'yellow' | 'neutral' {
|
|
49
|
+
if (changeType === 'merge' || changeType === 'rewire') return 'blue'
|
|
50
|
+
if (changeType === 'promote' || changeType === 'consolidate') return 'purple'
|
|
51
|
+
if (changeType === 'split') return 'yellow'
|
|
52
|
+
return 'neutral'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function consoleTone(state: RunState): 'neutral' | 'green' | 'red' | 'yellow' {
|
|
56
|
+
if (state === 'success') return 'green'
|
|
57
|
+
if (state === 'error') return 'red'
|
|
58
|
+
if (state === 'stopped') return 'yellow'
|
|
59
|
+
return 'neutral'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default function TopologyClient({
|
|
63
|
+
initialDashboard,
|
|
64
|
+
initialGovernanceState,
|
|
65
|
+
}: {
|
|
66
|
+
initialDashboard: TopologyDashboardSummary
|
|
67
|
+
initialGovernanceState: GovernanceState
|
|
68
|
+
}) {
|
|
69
|
+
const [dashboard, setDashboard] = useState(initialDashboard)
|
|
70
|
+
const [governanceState, setGovernanceState] = useState(initialGovernanceState)
|
|
71
|
+
const [runState, setRunState] = useState<RunState>('idle')
|
|
72
|
+
const [output, setOutput] = useState('')
|
|
73
|
+
const [activeActionLabel, setActiveActionLabel] = useState('topology action')
|
|
74
|
+
const [pendingDecision, setPendingDecision] = useState<string | null>(null)
|
|
75
|
+
const [pendingMode, setPendingMode] = useState<string | null>(null)
|
|
76
|
+
const [pendingPlanAction, setPendingPlanAction] = useState<string | null>(null)
|
|
77
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
78
|
+
const outputRef = useRef<HTMLPreElement | null>(null)
|
|
79
|
+
|
|
80
|
+
const handleOptimize = async () => {
|
|
81
|
+
setActiveActionLabel('topology review refresh')
|
|
82
|
+
setRunState('running')
|
|
83
|
+
setOutput('')
|
|
84
|
+
const controller = new AbortController()
|
|
85
|
+
abortRef.current = controller
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch('/api/run', {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ command: 'graph-optimize' }),
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (!res.body) {
|
|
96
|
+
setOutput('No response body')
|
|
97
|
+
setRunState('error')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const reader = res.body.getReader()
|
|
102
|
+
const decoder = new TextDecoder()
|
|
103
|
+
let sseBuffer = ''
|
|
104
|
+
|
|
105
|
+
while (true) {
|
|
106
|
+
const { done, value } = await reader.read()
|
|
107
|
+
if (done) break
|
|
108
|
+
|
|
109
|
+
sseBuffer += decoder.decode(value, { stream: true })
|
|
110
|
+
const events = sseBuffer.split('\n\n')
|
|
111
|
+
sseBuffer = events.pop() ?? ''
|
|
112
|
+
|
|
113
|
+
for (const event of events) {
|
|
114
|
+
const lines = event.split('\n')
|
|
115
|
+
let eventType = ''
|
|
116
|
+
let eventData = ''
|
|
117
|
+
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
if (line.startsWith('event: ')) eventType = line.slice(7)
|
|
120
|
+
if (line.startsWith('data: ')) eventData = line.slice(6)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (eventType === 'output' && eventData) {
|
|
124
|
+
try {
|
|
125
|
+
const text = JSON.parse(eventData) as string
|
|
126
|
+
setOutput((prev) => prev + text)
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight
|
|
129
|
+
}, 10)
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (eventType === 'done' && eventData) {
|
|
134
|
+
try {
|
|
135
|
+
const result = JSON.parse(JSON.parse(eventData) as string) as { success: boolean; stopped?: boolean }
|
|
136
|
+
setRunState(result.stopped ? 'stopped' : result.success ? 'success' : 'error')
|
|
137
|
+
} catch {
|
|
138
|
+
setRunState('success')
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const refreshed = await fetch('/api/topology-review', { cache: 'no-store' })
|
|
146
|
+
if (refreshed.ok) setDashboard(await refreshed.json())
|
|
147
|
+
} catch {}
|
|
148
|
+
setRunState((prev) => prev === 'running' ? 'success' : prev)
|
|
149
|
+
} catch (err: unknown) {
|
|
150
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
151
|
+
try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
|
|
152
|
+
setOutput((prev) => prev + '\n\n[Stopped by user]')
|
|
153
|
+
setRunState('stopped')
|
|
154
|
+
} else {
|
|
155
|
+
setOutput((prev) => prev || 'Network error')
|
|
156
|
+
setRunState('error')
|
|
157
|
+
}
|
|
158
|
+
} finally {
|
|
159
|
+
abortRef.current = null
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const handleStop = async () => {
|
|
164
|
+
if (abortRef.current) abortRef.current.abort()
|
|
165
|
+
try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const setGovernance = async (selectionMode: 'auto' | 'manual', manualMode?: GovernanceModeName) => {
|
|
169
|
+
const key = selectionMode === 'auto' ? 'auto' : manualMode ?? 'manual'
|
|
170
|
+
setPendingMode(key)
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch('/api/governance', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: { 'Content-Type': 'application/json' },
|
|
175
|
+
body: JSON.stringify({ selectionMode, manualMode }),
|
|
176
|
+
})
|
|
177
|
+
const data = await res.json()
|
|
178
|
+
if (data.success) {
|
|
179
|
+
setGovernanceState(data.state)
|
|
180
|
+
setDashboard((prev) => ({ ...prev, governance: data.summary }))
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
setPendingMode(null)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const handleDecision = async (candidateId: string, decision: TopologyReviewDecisionStatus) => {
|
|
188
|
+
setPendingDecision(candidateId)
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch('/api/topology-review', {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ candidateId, decision, rationale: `${decision} from topology control` }),
|
|
194
|
+
})
|
|
195
|
+
const data = await res.json()
|
|
196
|
+
if (data.success && data.dashboard) setDashboard(data.dashboard)
|
|
197
|
+
} finally {
|
|
198
|
+
setPendingDecision(null)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const handleExecutionAction = async (
|
|
203
|
+
action: 'prepare' | 'apply' | 'rollback',
|
|
204
|
+
payload: { candidateId?: string; planId?: string },
|
|
205
|
+
label: string,
|
|
206
|
+
) => {
|
|
207
|
+
setPendingPlanAction(payload.candidateId ?? payload.planId ?? action)
|
|
208
|
+
setActiveActionLabel(label)
|
|
209
|
+
setRunState('running')
|
|
210
|
+
setOutput('')
|
|
211
|
+
try {
|
|
212
|
+
const res = await fetch('/api/topology-apply', {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { 'Content-Type': 'application/json' },
|
|
215
|
+
body: JSON.stringify({ action, ...payload }),
|
|
216
|
+
})
|
|
217
|
+
const data = await res.json()
|
|
218
|
+
if (!res.ok || !data.success) {
|
|
219
|
+
setOutput(data.error ?? 'Topology action failed')
|
|
220
|
+
setRunState('error')
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
if (data.dashboard) setDashboard(data.dashboard)
|
|
224
|
+
setOutput(data.output || `${label} completed`)
|
|
225
|
+
setRunState('success')
|
|
226
|
+
} catch (err: unknown) {
|
|
227
|
+
setOutput(err instanceof Error ? err.message : 'Topology action failed')
|
|
228
|
+
setRunState('error')
|
|
229
|
+
} finally {
|
|
230
|
+
setPendingPlanAction(null)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div style={{ display: 'grid', gap: 22 }}>
|
|
236
|
+
<PageHero
|
|
237
|
+
eyebrow="Governed plasticity"
|
|
238
|
+
title="Topology Review Control"
|
|
239
|
+
description="Steer governance explicitly, refresh structural candidates from graph optimization, and move reviewed topology from manual-review into prepared, applied, and rollbackable structural execution."
|
|
240
|
+
chips={[
|
|
241
|
+
{ label: `${dashboard.summary.open} open reviews`, tone: dashboard.summary.open > 0 ? 'yellow' : 'green' },
|
|
242
|
+
{ label: `${dashboard.execution.prepared} prepared`, tone: dashboard.execution.prepared > 0 ? 'blue' : 'neutral' },
|
|
243
|
+
{ label: `${dashboard.execution.applied} applied`, tone: dashboard.execution.applied > 0 ? 'green' : 'neutral' },
|
|
244
|
+
{ label: formatMode(dashboard.governance.activeMode), tone: toneForMode(dashboard.governance.activeMode) },
|
|
245
|
+
{ label: dashboard.governance.source === 'operator-selected' ? 'operator-steered' : 'derived mode', tone: dashboard.governance.source === 'operator-selected' ? 'purple' : 'neutral' },
|
|
246
|
+
]}
|
|
247
|
+
actions={
|
|
248
|
+
<div style={{ display: 'grid', gap: 12 }}>
|
|
249
|
+
<div className="hero-note-card">
|
|
250
|
+
<div className="hero-note-label">Active governance profile</div>
|
|
251
|
+
<div className="hero-note-title">{formatMode(dashboard.governance.activeMode)}</div>
|
|
252
|
+
<div className="hero-note-copy">{dashboard.governance.profile.explanation}</div>
|
|
253
|
+
</div>
|
|
254
|
+
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
|
255
|
+
<button onClick={handleOptimize} disabled={runState === 'running'} className="badge badge-blue" style={{ border: 'none', cursor: 'pointer' }}>Run graph optimize</button>
|
|
256
|
+
<Link href="/coevolution" className="badge badge-gray">Open co-evolution</Link>
|
|
257
|
+
<Link href="/network" className="badge badge-gray">Open network</Link>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
}
|
|
261
|
+
/>
|
|
262
|
+
|
|
263
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 16 }}>
|
|
264
|
+
<MetricCard label="Open review items" value={dashboard.summary.open} sublabel={`${dashboard.summary.deferred} deferred • ${dashboard.summary.accepted} accepted`} tone={dashboard.summary.open > 0 ? 'yellow' : 'green'} icon="⇄" />
|
|
265
|
+
<MetricCard label="Accepted ready" value={dashboard.acceptedReady.length} sublabel="accepted candidates not yet prepared for execution" tone={dashboard.acceptedReady.length > 0 ? 'blue' : 'neutral'} icon="✓" />
|
|
266
|
+
<MetricCard label="Prepared plans" value={dashboard.execution.prepared} sublabel={`${dashboard.execution.safeToApply} safe-to-apply • ${dashboard.execution.prepareOnly} prepare-only`} tone={dashboard.execution.prepared > 0 ? 'blue' : 'neutral'} icon="◇" />
|
|
267
|
+
<MetricCard label="Applied plans" value={dashboard.execution.applied} sublabel={`${dashboard.execution.rolledBack} rolled back • ${dashboard.execution.artifacts} artifacts`} tone={dashboard.execution.applied > 0 ? 'green' : 'neutral'} icon="↑" />
|
|
268
|
+
<MetricCard label="Manual-route backlog" value={dashboard.summary.generatedFromManualReview} sublabel="pressure routed into actual operator review" tone="purple" icon="!" />
|
|
269
|
+
<MetricCard label="Snapshots" value={dashboard.execution.snapshots} sublabel="before/after state preserved for reviewed execution" tone="purple" icon="◎" />
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<SectionFrame
|
|
273
|
+
eyebrow="Execution pipeline"
|
|
274
|
+
title="Accepted review can now become prepared and applied structure"
|
|
275
|
+
description="Milestone 7 adds the missing transition layer between reviewed intent and safe structural execution. Only accepted candidates can enter plan preparation, and only safe plans can be applied."
|
|
276
|
+
>
|
|
277
|
+
<div className="grid-2">
|
|
278
|
+
<div style={{ display: 'grid', gap: 10 }}>
|
|
279
|
+
<div className="section-label">Accepted and ready to prepare</div>
|
|
280
|
+
{dashboard.acceptedReady.length > 0 ? dashboard.acceptedReady.map((candidate) => (
|
|
281
|
+
<div key={candidate.id} style={{ padding: '14px 16px', borderRadius: 18, border: '1px solid var(--border)', background: 'rgba(255,255,255,0.72)' }}>
|
|
282
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{candidate.title}</div>
|
|
283
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)', marginTop: 4, lineHeight: 1.6 }}>{candidate.reasonSummary}</div>
|
|
284
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 10 }}>
|
|
285
|
+
<span className={`badge badge-${toneForChange(candidate.changeType)}`}>{candidate.changeType}</span>
|
|
286
|
+
<span className="badge badge-gray">accepted</span>
|
|
287
|
+
<span className="badge badge-gray">risk {(candidate.riskScore * 100).toFixed(0)}%</span>
|
|
288
|
+
</div>
|
|
289
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 12 }}>
|
|
290
|
+
<button
|
|
291
|
+
onClick={() => handleExecutionAction('prepare', { candidateId: candidate.id }, `prepare ${candidate.title}`)}
|
|
292
|
+
disabled={pendingPlanAction === candidate.id}
|
|
293
|
+
className="badge badge-blue"
|
|
294
|
+
style={{ border: 'none', cursor: 'pointer' }}
|
|
295
|
+
>
|
|
296
|
+
Prepare apply plan
|
|
297
|
+
</button>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
)) : (
|
|
301
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
302
|
+
<div className="empty-state-title">No accepted candidates waiting for preparation</div>
|
|
303
|
+
<div className="empty-state-desc">Accept a topology review item first, then it will enter the reviewed execution pipeline here.</div>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<div style={{ display: 'grid', gap: 10 }}>
|
|
309
|
+
<div className="section-label">Prepared execution queue</div>
|
|
310
|
+
{dashboard.preparedQueue.length > 0 ? dashboard.preparedQueue.map((plan) => (
|
|
311
|
+
<div key={plan.id} style={{ padding: '14px 16px', borderRadius: 18, border: '1px solid var(--border)', background: 'rgba(255,255,255,0.72)' }}>
|
|
312
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{plan.changeType} plan</div>
|
|
313
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)', marginTop: 4, lineHeight: 1.6 }}>{plan.reasonSummary}</div>
|
|
314
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 10 }}>
|
|
315
|
+
<span className={`badge badge-${plan.safeToApply ? 'green' : 'yellow'}`}>{plan.executionMode}</span>
|
|
316
|
+
<span className="badge badge-gray">{plan.targetSkillSlugs.length} target nodes</span>
|
|
317
|
+
<span className="badge badge-gray">snapshot {plan.beforeSnapshotId.slice(-8)}</span>
|
|
318
|
+
</div>
|
|
319
|
+
<div style={{ display: 'grid', gap: 4, marginTop: 10 }}>
|
|
320
|
+
{plan.plannedGraphChanges.slice(0, 2).map((line, index) => (
|
|
321
|
+
<div key={`${plan.id}-graph-${index}`} className="signal-text">• {line}</div>
|
|
322
|
+
))}
|
|
323
|
+
{plan.warnings.slice(0, 2).map((line, index) => (
|
|
324
|
+
<div key={`${plan.id}-warn-${index}`} className="signal-text">• {line}</div>
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 12 }}>
|
|
328
|
+
{plan.safeToApply && plan.executionMode === 'apply' ? (
|
|
329
|
+
<button
|
|
330
|
+
onClick={() => handleExecutionAction('apply', { planId: plan.id }, `apply ${plan.id}`)}
|
|
331
|
+
disabled={pendingPlanAction === plan.id}
|
|
332
|
+
className="badge badge-green"
|
|
333
|
+
style={{ border: 'none', cursor: 'pointer' }}
|
|
334
|
+
>
|
|
335
|
+
Apply safely
|
|
336
|
+
</button>
|
|
337
|
+
) : (
|
|
338
|
+
<span className="badge badge-yellow">prepare-only</span>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
)) : (
|
|
343
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
344
|
+
<div className="empty-state-title">No prepared plans yet</div>
|
|
345
|
+
<div className="empty-state-desc">Prepared plans will appear here after you convert an accepted review candidate into an explicit apply plan.</div>
|
|
346
|
+
</div>
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</SectionFrame>
|
|
351
|
+
|
|
352
|
+
<div className="grid-2">
|
|
353
|
+
<SectionFrame
|
|
354
|
+
eyebrow="Governance steering"
|
|
355
|
+
title="Select the active mode"
|
|
356
|
+
description="You can leave governance in derived mode or pin the network to a specific operating bias. The selected mode affects structural review posture across the dashboard and runtime summaries."
|
|
357
|
+
>
|
|
358
|
+
<div style={{ display: 'grid', gap: 14 }}>
|
|
359
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
360
|
+
<button
|
|
361
|
+
onClick={() => setGovernance('auto')}
|
|
362
|
+
disabled={pendingMode !== null}
|
|
363
|
+
className={`hero-chip hero-chip-${governanceState.selectionMode === 'auto' ? 'green' : 'neutral'}`}
|
|
364
|
+
style={{ border: 'none', cursor: 'pointer' }}
|
|
365
|
+
>
|
|
366
|
+
Auto / derived
|
|
367
|
+
</button>
|
|
368
|
+
{GOVERNANCE_MODES.map((mode) => (
|
|
369
|
+
<button
|
|
370
|
+
key={mode}
|
|
371
|
+
onClick={() => setGovernance('manual', mode)}
|
|
372
|
+
disabled={pendingMode !== null}
|
|
373
|
+
className={`hero-chip hero-chip-${toneForMode(mode)}`}
|
|
374
|
+
style={{ border: governanceState.selectionMode === 'manual' && governanceState.manualMode === mode ? '1px solid rgba(0,0,0,0.12)' : 'none', cursor: 'pointer', opacity: pendingMode && pendingMode !== mode ? 0.55 : 1 }}
|
|
375
|
+
>
|
|
376
|
+
{formatMode(mode)}
|
|
377
|
+
</button>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div style={{ padding: '14px 16px', borderRadius: 18, background: 'rgba(255,255,255,0.72)', border: '1px solid var(--border)' }}>
|
|
382
|
+
<div className="section-label" style={{ marginBottom: 8 }}>Current steering state</div>
|
|
383
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}>
|
|
384
|
+
<span className={`hero-chip hero-chip-${toneForMode(dashboard.governance.activeMode)}`}>{formatMode(dashboard.governance.activeMode)}</span>
|
|
385
|
+
<span className="badge badge-gray">source: {dashboard.governance.source}</span>
|
|
386
|
+
<span className="badge badge-gray">derived: {formatMode(dashboard.governance.derivedMode)}</span>
|
|
387
|
+
<span className="badge badge-gray">review threshold {(dashboard.governance.profile.reviewThreshold * 100).toFixed(0)}%</span>
|
|
388
|
+
<span className="badge badge-gray">risk tolerance {(dashboard.governance.profile.riskTolerance * 100).toFixed(0)}%</span>
|
|
389
|
+
<span className="badge badge-gray">plasticity bias {(dashboard.governance.profile.plasticityBias * 100).toFixed(0)}%</span>
|
|
390
|
+
</div>
|
|
391
|
+
<div style={{ display: 'grid', gap: 6 }}>
|
|
392
|
+
{dashboard.governance.rationale.map((reason, index) => (
|
|
393
|
+
<div key={`${reason}-${index}`} className="signal-text">• {reason}</div>
|
|
394
|
+
))}
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</SectionFrame>
|
|
399
|
+
|
|
400
|
+
<SectionFrame
|
|
401
|
+
eyebrow="Recent decisions"
|
|
402
|
+
title="Accepted, deferred, and rejected changes"
|
|
403
|
+
description="Decisions are preserved as a truthful operator log rather than hidden automation."
|
|
404
|
+
>
|
|
405
|
+
<div className="summary-list">
|
|
406
|
+
{dashboard.recentDecisions.length > 0 ? dashboard.recentDecisions.map((candidate) => (
|
|
407
|
+
<div key={candidate.id} className="summary-row" style={{ alignItems: 'flex-start' }}>
|
|
408
|
+
<div className="summary-row-main">
|
|
409
|
+
<div className="summary-row-title">{candidate.title}</div>
|
|
410
|
+
<div className="summary-row-meta">{candidate.reasonSummary}</div>
|
|
411
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 8 }}>
|
|
412
|
+
<span className={`badge badge-${toneForStatus(candidate.status)}`}>{candidate.status}</span>
|
|
413
|
+
<span className={`badge badge-${toneForChange(candidate.changeType)}`}>{candidate.changeType}</span>
|
|
414
|
+
<span className="badge badge-gray">{candidate.source}</span>
|
|
415
|
+
<span className="badge badge-gray">confidence {(candidate.confidence * 100).toFixed(0)}%</span>
|
|
416
|
+
</div>
|
|
417
|
+
{candidate.latestDecision ? (
|
|
418
|
+
<div style={{ display: 'grid', gap: 4, marginTop: 10 }}>
|
|
419
|
+
<div className="signal-text">• {candidate.latestDecision.rationale}</div>
|
|
420
|
+
<div className="signal-text">• Decided {formatDate(candidate.latestDecision.decidedAt)} under {formatMode(candidate.latestDecision.governanceMode)}</div>
|
|
421
|
+
</div>
|
|
422
|
+
) : null}
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
)) : (
|
|
426
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
427
|
+
<div className="empty-state-title">No topology decisions yet</div>
|
|
428
|
+
<div className="empty-state-desc">Run graph optimize to populate the queue, then accept, defer, or reject structural candidates from here.</div>
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
</div>
|
|
432
|
+
</SectionFrame>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
{runState !== 'idle' ? (
|
|
436
|
+
<ConsolePanel
|
|
437
|
+
tone={consoleTone(runState)}
|
|
438
|
+
title={<span>{runState === 'running' ? `Running ${activeActionLabel}…` : runState === 'success' ? `${activeActionLabel} completed` : runState === 'stopped' ? `${activeActionLabel} stopped` : `${activeActionLabel} failed`}</span>}
|
|
439
|
+
actions={runState === 'running'
|
|
440
|
+
? <button onClick={handleStop} className="badge badge-red" style={{ border: 'none', cursor: 'pointer' }}>Stop</button>
|
|
441
|
+
: <button onClick={() => window.location.reload()} className="badge badge-gray" style={{ border: 'none', cursor: 'pointer' }}>Refresh page</button>}
|
|
442
|
+
>
|
|
443
|
+
<pre ref={outputRef} style={{ margin: 0, padding: '16px 18px', fontSize: 11.5, lineHeight: 1.62, color: 'var(--text-secondary)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
444
|
+
{output || 'Waiting for command output…'}
|
|
445
|
+
</pre>
|
|
446
|
+
</ConsolePanel>
|
|
447
|
+
) : null}
|
|
448
|
+
|
|
449
|
+
<SectionFrame
|
|
450
|
+
eyebrow="Manual review queue"
|
|
451
|
+
title="Open topology backlog"
|
|
452
|
+
description="This is the real operator lane for structural plasticity. Review items are driven by graph optimize, recurring motifs, realized transfer evidence, and manual-review pressure routing."
|
|
453
|
+
>
|
|
454
|
+
<div style={{ display: 'grid', gap: 10 }}>
|
|
455
|
+
{dashboard.backlog.length > 0 ? dashboard.backlog.map((candidate) => (
|
|
456
|
+
<div key={candidate.id} style={{ padding: '14px 16px', borderRadius: 18, border: '1px solid var(--border)', background: 'rgba(255,255,255,0.72)' }}>
|
|
457
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 14, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
|
458
|
+
<div style={{ flex: 1, minWidth: 280 }}>
|
|
459
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{candidate.title}</div>
|
|
460
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)', lineHeight: 1.6, marginTop: 4 }}>{candidate.reasonSummary}</div>
|
|
461
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 10 }}>
|
|
462
|
+
<span className={`badge badge-${toneForStatus(candidate.status)}`}>{candidate.status}</span>
|
|
463
|
+
<span className={`badge badge-${toneForChange(candidate.changeType)}`}>{candidate.changeType}</span>
|
|
464
|
+
<span className="badge badge-gray">{candidate.priority}</span>
|
|
465
|
+
<span className="badge badge-gray">{candidate.source}</span>
|
|
466
|
+
<span className="badge badge-gray">confidence {(candidate.confidence * 100).toFixed(0)}%</span>
|
|
467
|
+
<span className="badge badge-gray">risk {(candidate.riskScore * 100).toFixed(0)}%</span>
|
|
468
|
+
{(candidate.projectIds ?? []).slice(0, 3).map((projectId) => <span key={`${candidate.id}-${projectId}`} className="badge badge-gray">{projectId}</span>)}
|
|
469
|
+
</div>
|
|
470
|
+
<div style={{ display: 'grid', gap: 4, marginTop: 10 }}>
|
|
471
|
+
{candidate.evidence.slice(0, 3).map((line, index) => (
|
|
472
|
+
<div key={`${candidate.id}-evidence-${index}`} className="signal-text">• {line}</div>
|
|
473
|
+
))}
|
|
474
|
+
</div>
|
|
475
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 12 }}>
|
|
476
|
+
<button onClick={() => handleDecision(candidate.id, 'accepted')} disabled={pendingDecision === candidate.id} className="badge badge-green" style={{ border: 'none', cursor: 'pointer' }}>Accept</button>
|
|
477
|
+
<button onClick={() => handleDecision(candidate.id, 'deferred')} disabled={pendingDecision === candidate.id} className="badge badge-purple" style={{ border: 'none', cursor: 'pointer' }}>Defer</button>
|
|
478
|
+
<button onClick={() => handleDecision(candidate.id, 'rejected')} disabled={pendingDecision === candidate.id} className="badge badge-gray" style={{ border: 'none', cursor: 'pointer' }}>Reject</button>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
<div style={{ minWidth: 180, textAlign: 'right', fontSize: 11.5, color: 'var(--text-dim)', lineHeight: 1.7 }}>
|
|
482
|
+
<div>Observed {formatDate(candidate.lastObservedAt)}</div>
|
|
483
|
+
<div>Last activity {formatDate(candidate.lastActivityAt)}</div>
|
|
484
|
+
<div>Governance {formatMode(candidate.governanceMode)}</div>
|
|
485
|
+
<div>{candidate.affectedNodeIds.length} affected nodes</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
)) : (
|
|
490
|
+
<div className="empty-state" style={{ padding: 28 }}>
|
|
491
|
+
<div className="empty-state-title">No topology backlog yet</div>
|
|
492
|
+
<div className="empty-state-desc">Run <code>graph --optimize</code> from this page to refresh structural review candidates and turn manual-review into a populated operator lane.</div>
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
</SectionFrame>
|
|
497
|
+
|
|
498
|
+
<SectionFrame
|
|
499
|
+
eyebrow="Applied history"
|
|
500
|
+
title="Reviewed structural transitions"
|
|
501
|
+
description="Applied plans remain visible with their latest status, snapshot references, and rollback continuity."
|
|
502
|
+
>
|
|
503
|
+
<div className="summary-list">
|
|
504
|
+
{dashboard.appliedHistory.length > 0 ? dashboard.appliedHistory.map((plan) => (
|
|
505
|
+
<div key={plan.id} className="summary-row" style={{ alignItems: 'flex-start' }}>
|
|
506
|
+
<div className="summary-row-main">
|
|
507
|
+
<div className="summary-row-title">{plan.changeType} · {plan.id}</div>
|
|
508
|
+
<div className="summary-row-meta">{plan.reasonSummary}</div>
|
|
509
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 8 }}>
|
|
510
|
+
<span className={`badge badge-${plan.latestStatus === 'applied' ? 'green' : plan.latestStatus === 'rolled-back' ? 'purple' : 'gray'}`}>{plan.latestStatus}</span>
|
|
511
|
+
<span className="badge badge-gray">{plan.executionMode}</span>
|
|
512
|
+
<span className="badge badge-gray">snapshot {plan.beforeSnapshotId.slice(-8)}</span>
|
|
513
|
+
{plan.latestExecution?.afterSnapshotId ? <span className="badge badge-gray">after {plan.latestExecution.afterSnapshotId.slice(-8)}</span> : null}
|
|
514
|
+
</div>
|
|
515
|
+
<div style={{ display: 'grid', gap: 4, marginTop: 10 }}>
|
|
516
|
+
{plan.plannedGraphChanges.slice(0, 2).map((line, index) => (
|
|
517
|
+
<div key={`${plan.id}-history-${index}`} className="signal-text">• {line}</div>
|
|
518
|
+
))}
|
|
519
|
+
{(plan.latestExecution?.notes ?? []).slice(0, 2).map((line, index) => (
|
|
520
|
+
<div key={`${plan.id}-note-${index}`} className="signal-text">• {line}</div>
|
|
521
|
+
))}
|
|
522
|
+
</div>
|
|
523
|
+
{plan.latestStatus === 'applied' ? (
|
|
524
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 12 }}>
|
|
525
|
+
<button
|
|
526
|
+
onClick={() => handleExecutionAction('rollback', { planId: plan.id }, `rollback ${plan.id}`)}
|
|
527
|
+
disabled={pendingPlanAction === plan.id}
|
|
528
|
+
className="badge badge-purple"
|
|
529
|
+
style={{ border: 'none', cursor: 'pointer' }}
|
|
530
|
+
>
|
|
531
|
+
Roll back
|
|
532
|
+
</button>
|
|
533
|
+
</div>
|
|
534
|
+
) : null}
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
)) : (
|
|
538
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
539
|
+
<div className="empty-state-title">No applied topology history yet</div>
|
|
540
|
+
<div className="empty-state-desc">Once a prepared safe plan is applied, it will appear here with rollback continuity.</div>
|
|
541
|
+
</div>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
</SectionFrame>
|
|
545
|
+
|
|
546
|
+
<SectionFrame
|
|
547
|
+
eyebrow="Ledger"
|
|
548
|
+
title="All recorded topology candidates"
|
|
549
|
+
description="A compact ledger of the full queue, including open, deferred, accepted, and rejected structural candidates."
|
|
550
|
+
>
|
|
551
|
+
<div className="summary-list">
|
|
552
|
+
{dashboard.candidates.length > 0 ? dashboard.candidates.map((candidate) => (
|
|
553
|
+
<div key={candidate.id} className="summary-row" style={{ alignItems: 'flex-start' }}>
|
|
554
|
+
<div className="summary-row-main">
|
|
555
|
+
<div className="summary-row-title">{candidate.title}</div>
|
|
556
|
+
<div className="summary-row-meta">{candidate.description ?? candidate.reasonSummary}</div>
|
|
557
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 8 }}>
|
|
558
|
+
<span className={`badge badge-${toneForStatus(candidate.status)}`}>{candidate.status}</span>
|
|
559
|
+
<span className={`badge badge-${toneForChange(candidate.changeType)}`}>{candidate.changeType}</span>
|
|
560
|
+
<span className="badge badge-gray">{candidate.source}</span>
|
|
561
|
+
<span className="badge badge-gray">{candidate.affectedNodeIds.length} nodes</span>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
)) : (
|
|
566
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
567
|
+
<div className="empty-state-title">No structural candidates stored yet</div>
|
|
568
|
+
<div className="empty-state-desc">The ledger will populate after the first topology refresh.</div>
|
|
569
|
+
</div>
|
|
570
|
+
)}
|
|
571
|
+
</div>
|
|
572
|
+
</SectionFrame>
|
|
573
|
+
</div>
|
|
574
|
+
)
|
|
575
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { loadGovernanceState, loadTopologyDashboardSummary } from '@/lib/data'
|
|
2
|
+
import TopologyClient from './client'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export default function TopologyPage() {
|
|
7
|
+
const dashboard = loadTopologyDashboardSummary()
|
|
8
|
+
const governanceState = loadGovernanceState()
|
|
9
|
+
return <TopologyClient initialDashboard={dashboard} initialGovernanceState={governanceState} />
|
|
10
|
+
}
|
|
@@ -12,6 +12,8 @@ interface NavItem {
|
|
|
12
12
|
const WORKSPACE_NAV: NavItem[] = [
|
|
13
13
|
{ href: '/', label: 'Overview', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1' },
|
|
14
14
|
{ href: '/network', label: 'Skill Network', icon: 'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1' },
|
|
15
|
+
{ href: '/topology', label: 'Topology', icon: 'M4 7h6m4 0h6M7 4v6m10-6v6M5 17h14M12 10v10' },
|
|
16
|
+
{ href: '/coevolution', label: 'Co-Evolution', icon: 'M4 12h4m8 0h4M12 4v4m0 8v4M7.05 7.05l2.83 2.83m4.24 4.24l2.83 2.83m0-9.9l-2.83 2.83m-4.24 4.24l-2.83 2.83' },
|
|
15
17
|
{ href: '/evolution', label: 'Evolution', icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
|
|
16
18
|
{ href: '/research', label: 'Research', icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' },
|
|
17
19
|
{ href: '/frontier', label: 'Frontier', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
|