helixevo 0.2.0 → 0.2.1
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/dashboard/app/api/skills/route.ts +298 -0
- package/dashboard/app/evolution/page.tsx +128 -0
- package/dashboard/app/frontier/page.tsx +128 -0
- package/dashboard/app/globals.css +1211 -0
- package/dashboard/app/guide/page.tsx +984 -0
- package/dashboard/app/layout.tsx +79 -0
- package/dashboard/app/network/client.tsx +1090 -0
- package/dashboard/app/network/page.tsx +68 -0
- package/dashboard/app/page.tsx +147 -0
- package/dashboard/app/research/page.tsx +124 -0
- package/dashboard/components/SkillFlowNode.tsx +192 -0
- package/dashboard/lib/data.ts +146 -0
- package/dashboard/next-env.d.ts +6 -0
- package/dashboard/package-lock.json +1182 -0
- package/dashboard/package.json +21 -0
- package/dashboard/tsconfig.json +40 -0
- package/dist/cli.js +44 -9
- package/package.json +8 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, readdirSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { homedir } from 'os'
|
|
5
|
+
|
|
6
|
+
const HELIX_DIR = join(homedir(), '.helix')
|
|
7
|
+
const GENERAL_DIR = join(HELIX_DIR, 'general')
|
|
8
|
+
|
|
9
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function readSkillMd(slug: string): { raw: string; meta: Record<string, unknown>; content: string } | null {
|
|
12
|
+
const path = join(GENERAL_DIR, slug, 'SKILL.md')
|
|
13
|
+
if (!existsSync(path)) return null
|
|
14
|
+
const raw = readFileSync(path, 'utf-8')
|
|
15
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
16
|
+
if (!match) return { raw, meta: {}, content: raw }
|
|
17
|
+
|
|
18
|
+
// Simple YAML parse (avoid importing yaml in API route)
|
|
19
|
+
const meta: Record<string, unknown> = {}
|
|
20
|
+
for (const line of match[1].split('\n')) {
|
|
21
|
+
const m = line.match(/^(\w[\w-]*?):\s*(.*)/)
|
|
22
|
+
if (m) {
|
|
23
|
+
let val: unknown = m[2].trim()
|
|
24
|
+
if (val === 'true') val = true
|
|
25
|
+
else if (val === 'false') val = false
|
|
26
|
+
else if (/^\d+(\.\d+)?$/.test(val as string)) val = parseFloat(val as string)
|
|
27
|
+
else if ((val as string).startsWith('[')) {
|
|
28
|
+
try { val = JSON.parse((val as string).replace(/'/g, '"')) } catch { /* keep as string */ }
|
|
29
|
+
}
|
|
30
|
+
// Remove quotes
|
|
31
|
+
if (typeof val === 'string' && val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1)
|
|
32
|
+
meta[m[1]] = val
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { raw, meta, content: match[2].trim() }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function loadGraph(): { nodes: { id: string; layer: string }[]; edges: { from: string; to: string; type: string }[] } {
|
|
40
|
+
const path = join(HELIX_DIR, 'skill-graph.json')
|
|
41
|
+
if (!existsSync(path)) return { nodes: [], edges: [] }
|
|
42
|
+
return JSON.parse(readFileSync(path, 'utf-8'))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listSkills(): string[] {
|
|
46
|
+
if (!existsSync(GENERAL_DIR)) return []
|
|
47
|
+
return readdirSync(GENERAL_DIR, { withFileTypes: true })
|
|
48
|
+
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
|
49
|
+
.map(d => d.name)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Network adaptation analysis ────────────────────────────────
|
|
53
|
+
|
|
54
|
+
interface AdaptationResult {
|
|
55
|
+
status: 'ok' | 'warning' | 'action-needed'
|
|
56
|
+
messages: { type: 'info' | 'warning' | 'action'; text: string }[]
|
|
57
|
+
suggestions: { type: 'create' | 'merge' | 'promote' | 'rewire'; description: string }[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function analyzeNetworkAdaptation(
|
|
61
|
+
action: 'update' | 'delete' | 'create' | 'promote',
|
|
62
|
+
slug: string,
|
|
63
|
+
newLayer?: string
|
|
64
|
+
): AdaptationResult {
|
|
65
|
+
const graph = loadGraph()
|
|
66
|
+
const result: AdaptationResult = { status: 'ok', messages: [], suggestions: [] }
|
|
67
|
+
|
|
68
|
+
if (action === 'delete') {
|
|
69
|
+
// Check what depends on this skill
|
|
70
|
+
const dependents = graph.edges.filter(e => e.from === slug && e.type === 'inherits')
|
|
71
|
+
const enhancedBy = graph.edges.filter(e => (e.from === slug || e.to === slug) && e.type === 'enhances')
|
|
72
|
+
const dependencies = graph.edges.filter(e => e.to === slug && e.type === 'depends')
|
|
73
|
+
|
|
74
|
+
if (dependents.length > 0) {
|
|
75
|
+
result.status = 'action-needed'
|
|
76
|
+
result.messages.push({
|
|
77
|
+
type: 'warning',
|
|
78
|
+
text: `${dependents.length} skill(s) inherit from "${slug}": ${dependents.map(e => e.to).join(', ')}. They will become orphans.`
|
|
79
|
+
})
|
|
80
|
+
result.suggestions.push({
|
|
81
|
+
type: 'rewire',
|
|
82
|
+
description: `Re-assign children [${dependents.map(e => e.to).join(', ')}] to another parent before deleting`
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (dependencies.length > 0) {
|
|
87
|
+
result.status = 'warning'
|
|
88
|
+
result.messages.push({
|
|
89
|
+
type: 'warning',
|
|
90
|
+
text: `${dependencies.length} skill(s) depend on "${slug}": ${dependencies.map(e => e.from).join(', ')}`
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (enhancedBy.length > 0) {
|
|
95
|
+
result.messages.push({
|
|
96
|
+
type: 'info',
|
|
97
|
+
text: `${enhancedBy.length} enhancement edge(s) will be removed`
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if deletion creates a coverage gap
|
|
102
|
+
const allSlugs = listSkills()
|
|
103
|
+
if (allSlugs.length <= 3) {
|
|
104
|
+
result.status = 'warning'
|
|
105
|
+
result.messages.push({
|
|
106
|
+
type: 'warning',
|
|
107
|
+
text: 'Removing this skill will leave very few skills. Consider merging instead of deleting.'
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (action === 'update') {
|
|
113
|
+
// Check if co-evolved skills might be affected
|
|
114
|
+
const coEvolved = graph.edges.filter(
|
|
115
|
+
e => (e.from === slug || e.to === slug) && (e.type === 'co-evolves' || e.type === 'enhances')
|
|
116
|
+
)
|
|
117
|
+
if (coEvolved.length > 0) {
|
|
118
|
+
const partners = coEvolved.map(e => e.from === slug ? e.to : e.from)
|
|
119
|
+
result.messages.push({
|
|
120
|
+
type: 'info',
|
|
121
|
+
text: `This skill co-evolves with [${partners.join(', ')}]. Changes may affect their behavior.`
|
|
122
|
+
})
|
|
123
|
+
result.suggestions.push({
|
|
124
|
+
type: 'rewire',
|
|
125
|
+
description: `Review golden cases for [${partners.join(', ')}] after this change`
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (action === 'promote') {
|
|
131
|
+
// Check if promotion makes sense
|
|
132
|
+
const currentSkill = readSkillMd(slug)
|
|
133
|
+
const currentLayer = (currentSkill?.meta?.layer as string) ?? 'domain'
|
|
134
|
+
|
|
135
|
+
if (newLayer === 'system' && currentLayer === 'project') {
|
|
136
|
+
result.status = 'warning'
|
|
137
|
+
result.messages.push({
|
|
138
|
+
type: 'warning',
|
|
139
|
+
text: 'Promoting directly from project to system. Consider promoting to domain first.'
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if similar domain skill already exists
|
|
144
|
+
const allSlugs = listSkills()
|
|
145
|
+
const domainSkills = allSlugs.filter(s => {
|
|
146
|
+
const sk = readSkillMd(s)
|
|
147
|
+
return sk?.meta?.layer === 'domain' && s !== slug
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (domainSkills.length > 0 && newLayer === 'domain') {
|
|
151
|
+
result.messages.push({
|
|
152
|
+
type: 'info',
|
|
153
|
+
text: `${domainSkills.length} domain skill(s) exist. Check for overlap before promoting.`
|
|
154
|
+
})
|
|
155
|
+
result.suggestions.push({
|
|
156
|
+
type: 'merge',
|
|
157
|
+
description: `Consider merging with existing domain skills instead of creating a new one`
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (action === 'create') {
|
|
163
|
+
// Check if similar skill already exists
|
|
164
|
+
const existingSlugs = listSkills()
|
|
165
|
+
if (existingSlugs.includes(slug)) {
|
|
166
|
+
result.status = 'warning'
|
|
167
|
+
result.messages.push({
|
|
168
|
+
type: 'warning',
|
|
169
|
+
text: `Skill "${slug}" already exists. This will overwrite it.`
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Suggest connecting to existing skills
|
|
174
|
+
if (graph.nodes.length > 0) {
|
|
175
|
+
result.suggestions.push({
|
|
176
|
+
type: 'rewire',
|
|
177
|
+
description: 'After creating, run "helixevo graph --rebuild" to detect relationships with existing skills'
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (result.messages.length === 0) {
|
|
183
|
+
result.messages.push({ type: 'info', text: 'Network can adapt to this change without issues.' })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── GET: List all skills or get one ────────────────────────────
|
|
190
|
+
|
|
191
|
+
export async function GET(request: NextRequest) {
|
|
192
|
+
const slug = request.nextUrl.searchParams.get('slug')
|
|
193
|
+
|
|
194
|
+
if (slug) {
|
|
195
|
+
const skill = readSkillMd(slug)
|
|
196
|
+
if (!skill) return NextResponse.json({ error: 'Skill not found' }, { status: 404 })
|
|
197
|
+
return NextResponse.json({ slug, ...skill })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// List all skills
|
|
201
|
+
const slugs = listSkills()
|
|
202
|
+
const skills = slugs.map(s => {
|
|
203
|
+
const skill = readSkillMd(s)
|
|
204
|
+
return {
|
|
205
|
+
slug: s,
|
|
206
|
+
name: skill?.meta?.name ?? s,
|
|
207
|
+
description: skill?.meta?.description ?? '',
|
|
208
|
+
layer: skill?.meta?.layer ?? 'domain',
|
|
209
|
+
score: skill?.meta?.score ?? 0.7,
|
|
210
|
+
generation: skill?.meta?.generation ?? 0,
|
|
211
|
+
tags: skill?.meta?.tags ?? [],
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return NextResponse.json({ skills })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── PUT: Update a skill ────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export async function PUT(request: NextRequest) {
|
|
221
|
+
const body = await request.json()
|
|
222
|
+
const { slug, content, layer } = body
|
|
223
|
+
|
|
224
|
+
if (!slug) return NextResponse.json({ error: 'slug required' }, { status: 400 })
|
|
225
|
+
|
|
226
|
+
// Promote/demote layer
|
|
227
|
+
if (layer && !content) {
|
|
228
|
+
const skill = readSkillMd(slug)
|
|
229
|
+
if (!skill) return NextResponse.json({ error: 'Skill not found' }, { status: 404 })
|
|
230
|
+
|
|
231
|
+
const adaptation = analyzeNetworkAdaptation('promote', slug, layer)
|
|
232
|
+
|
|
233
|
+
// Update layer in the raw file
|
|
234
|
+
const updated = skill.raw.replace(
|
|
235
|
+
/^(layer:\s*).*$/m,
|
|
236
|
+
`$1${layer}`
|
|
237
|
+
)
|
|
238
|
+
// If no layer field, add it after description
|
|
239
|
+
const finalContent = updated.includes('layer:')
|
|
240
|
+
? updated
|
|
241
|
+
: skill.raw.replace(/(description:.*\n)/, `$1layer: ${layer}\n`)
|
|
242
|
+
|
|
243
|
+
const skillPath = join(GENERAL_DIR, slug, 'SKILL.md')
|
|
244
|
+
writeFileSync(skillPath, finalContent)
|
|
245
|
+
|
|
246
|
+
return NextResponse.json({ success: true, slug, layer, adaptation })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Update content
|
|
250
|
+
if (content) {
|
|
251
|
+
const adaptation = analyzeNetworkAdaptation('update', slug)
|
|
252
|
+
const skillDir = join(GENERAL_DIR, slug)
|
|
253
|
+
if (!existsSync(skillDir)) mkdirSync(skillDir, { recursive: true })
|
|
254
|
+
writeFileSync(join(skillDir, 'SKILL.md'), content)
|
|
255
|
+
|
|
256
|
+
return NextResponse.json({ success: true, slug, adaptation })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return NextResponse.json({ error: 'content or layer required' }, { status: 400 })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── POST: Create a new skill ───────────────────────────────────
|
|
263
|
+
|
|
264
|
+
export async function POST(request: NextRequest) {
|
|
265
|
+
const body = await request.json()
|
|
266
|
+
const { slug, content } = body
|
|
267
|
+
|
|
268
|
+
if (!slug || !content) {
|
|
269
|
+
return NextResponse.json({ error: 'slug and content required' }, { status: 400 })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const adaptation = analyzeNetworkAdaptation('create', slug)
|
|
273
|
+
|
|
274
|
+
const skillDir = join(GENERAL_DIR, slug)
|
|
275
|
+
mkdirSync(skillDir, { recursive: true })
|
|
276
|
+
writeFileSync(join(skillDir, 'SKILL.md'), content)
|
|
277
|
+
|
|
278
|
+
return NextResponse.json({ success: true, slug, adaptation })
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── DELETE: Remove a skill ─────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
export async function DELETE(request: NextRequest) {
|
|
284
|
+
const slug = request.nextUrl.searchParams.get('slug')
|
|
285
|
+
if (!slug) return NextResponse.json({ error: 'slug required' }, { status: 400 })
|
|
286
|
+
|
|
287
|
+
const skillDir = join(GENERAL_DIR, slug)
|
|
288
|
+
if (!existsSync(skillDir)) {
|
|
289
|
+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 })
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Analyze impact BEFORE deleting
|
|
293
|
+
const adaptation = analyzeNetworkAdaptation('delete', slug)
|
|
294
|
+
|
|
295
|
+
rmSync(skillDir, { recursive: true })
|
|
296
|
+
|
|
297
|
+
return NextResponse.json({ success: true, slug, adaptation })
|
|
298
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { loadHistory } from '@/lib/data'
|
|
2
|
+
|
|
3
|
+
export const dynamic = 'force-dynamic'
|
|
4
|
+
|
|
5
|
+
export default function EvolutionPage() {
|
|
6
|
+
const history = loadHistory()
|
|
7
|
+
const iterations = [...history.iterations].reverse()
|
|
8
|
+
const total = iterations.flatMap(i => i.proposals)
|
|
9
|
+
const accepted = total.filter(p => p.outcome === 'accepted').length
|
|
10
|
+
const rejected = total.filter(p => p.outcome === 'rejected').length
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
<div className="page-header">
|
|
15
|
+
<h1 className="page-title">Evolution Timeline</h1>
|
|
16
|
+
<p className="page-desc">
|
|
17
|
+
{iterations.length} runs · {accepted} accepted · {rejected} rejected
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{/* Summary Stats */}
|
|
22
|
+
<div className="grid-3" style={{ marginBottom: 28 }}>
|
|
23
|
+
<div className="stat-card">
|
|
24
|
+
<div className="stat-value" style={{ color: 'var(--purple)' }}>{iterations.length}</div>
|
|
25
|
+
<div className="stat-label">Evolution Runs</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div className="stat-card">
|
|
28
|
+
<div className="stat-value" style={{ color: 'var(--green)' }}>{accepted}</div>
|
|
29
|
+
<div className="stat-label">Proposals Accepted</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div className="stat-card">
|
|
32
|
+
<div className="stat-value" style={{ color: 'var(--red)' }}>{rejected}</div>
|
|
33
|
+
<div className="stat-label">Proposals Rejected</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{/* Timeline */}
|
|
38
|
+
<div style={{ paddingLeft: 20, position: 'relative' }}>
|
|
39
|
+
<div style={{ position: 'absolute', left: 9, top: 0, bottom: 0, width: 2, background: 'var(--border)' }} />
|
|
40
|
+
|
|
41
|
+
{iterations.map(iter => (
|
|
42
|
+
<div key={iter.id} style={{ position: 'relative', marginBottom: 24, paddingLeft: 28 }}>
|
|
43
|
+
{/* Timeline dot */}
|
|
44
|
+
<div style={{
|
|
45
|
+
position: 'absolute', left: -1, top: 4, width: 14, height: 14,
|
|
46
|
+
borderRadius: '50%', background: 'var(--purple)', border: '3px solid var(--bg)',
|
|
47
|
+
boxShadow: '0 0 0 2px var(--purple-light)',
|
|
48
|
+
}} />
|
|
49
|
+
|
|
50
|
+
{/* Run Header */}
|
|
51
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 10 }}>
|
|
52
|
+
<span style={{ fontSize: 15, fontWeight: 700 }}>{iter.id}</span>
|
|
53
|
+
<span style={{ fontSize: 12, color: 'var(--text-dim)' }}>
|
|
54
|
+
{new Date(iter.timestamp).toLocaleString()} · {iter.trigger} · {iter.failureCount} failures
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Clusters */}
|
|
59
|
+
{iter.failureClusters.length > 0 && (
|
|
60
|
+
<div style={{ display: 'flex', gap: 6, marginBottom: 10, flexWrap: 'wrap' }}>
|
|
61
|
+
{iter.failureClusters.map((c, i) => (
|
|
62
|
+
<span key={i} className="badge badge-blue">{c.type} ({c.count})</span>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{/* Proposals */}
|
|
68
|
+
{iter.proposals.map(p => (
|
|
69
|
+
<div key={p.id} className="proposal-card" style={{
|
|
70
|
+
borderLeft: `3px solid ${p.outcome === 'accepted' ? 'var(--green)' : 'var(--red)'}`,
|
|
71
|
+
}}>
|
|
72
|
+
<div style={{ padding: '12px 16px' }}>
|
|
73
|
+
{/* Proposal Header */}
|
|
74
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
|
75
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
76
|
+
<span className={`badge ${p.outcome === 'accepted' ? 'badge-green' : 'badge-red'}`}>
|
|
77
|
+
{p.outcome}
|
|
78
|
+
</span>
|
|
79
|
+
<span style={{ fontSize: 14, fontWeight: 600 }}>{p.targetSkill}</span>
|
|
80
|
+
<span className="badge badge-gray">{p.action}</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Description */}
|
|
85
|
+
<p style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 10, lineHeight: 1.5 }}>
|
|
86
|
+
{p.description.slice(0, 200)}
|
|
87
|
+
</p>
|
|
88
|
+
|
|
89
|
+
{/* Judge Scores */}
|
|
90
|
+
<div style={{ display: 'flex', gap: 10, marginBottom: 6 }}>
|
|
91
|
+
{[
|
|
92
|
+
{ label: 'Task', score: p.judges.taskCompletion.score, color: 'var(--green)' },
|
|
93
|
+
{ label: 'Align', score: p.judges.correctionAlignment.score, color: 'var(--blue)' },
|
|
94
|
+
{ label: 'Side-effects', score: p.judges.sideEffectCheck.score, color: 'var(--purple)' },
|
|
95
|
+
].map(j => (
|
|
96
|
+
<div key={j.label} style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}>
|
|
97
|
+
<span style={{ color: 'var(--text-dim)' }}>{j.label}:</span>
|
|
98
|
+
<b style={{ color: j.score >= 7 ? j.color : 'var(--red)' }}>{j.score}/10</b>
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
{p.regressionResult.total > 0 && (
|
|
102
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}>
|
|
103
|
+
<span style={{ color: 'var(--text-dim)' }}>Regression:</span>
|
|
104
|
+
<b>{p.regressionResult.passed}/{p.regressionResult.total}</b>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Outcome Reason */}
|
|
110
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.4 }}>{p.outcomeReason}</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
))}
|
|
116
|
+
|
|
117
|
+
{iterations.length === 0 && (
|
|
118
|
+
<div className="card" style={{ marginLeft: 28 }}>
|
|
119
|
+
<div className="empty-state">
|
|
120
|
+
<div className="empty-state-title">No evolution runs yet</div>
|
|
121
|
+
<div className="empty-state-desc">Run <code>helixevo evolve</code> to start the evolution loop</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { loadFrontier, loadCanaries } from '@/lib/data'
|
|
2
|
+
|
|
3
|
+
export const dynamic = 'force-dynamic'
|
|
4
|
+
|
|
5
|
+
export default function FrontierPage() {
|
|
6
|
+
const frontier = loadFrontier()
|
|
7
|
+
const canaries = loadCanaries()
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<div className="page-header">
|
|
12
|
+
<h1 className="page-title">Pareto Frontier</h1>
|
|
13
|
+
<p className="page-desc">
|
|
14
|
+
Top {frontier.capacity} skill configurations ranked by composite score
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{/* Frontier Programs */}
|
|
19
|
+
<div style={{ display: 'grid', gap: 12, marginBottom: 28 }}>
|
|
20
|
+
{frontier.programs.map((p, rank) => (
|
|
21
|
+
<div key={`${p.id}-${rank}`} className="card" style={{ overflow: 'hidden' }}>
|
|
22
|
+
<div className="card-body" style={{ position: 'relative' }}>
|
|
23
|
+
{/* Rank Badge */}
|
|
24
|
+
<div style={{
|
|
25
|
+
position: 'absolute', top: 0, right: 0,
|
|
26
|
+
padding: '4px 16px',
|
|
27
|
+
background: rank === 0 ? 'var(--purple)' : rank === 1 ? 'var(--blue)' : 'var(--bg-section)',
|
|
28
|
+
color: rank <= 1 ? '#fff' : 'var(--text-dim)',
|
|
29
|
+
borderBottomLeftRadius: 10, fontSize: 12, fontWeight: 700,
|
|
30
|
+
}}>
|
|
31
|
+
#{rank + 1}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* Header */}
|
|
35
|
+
<div style={{ marginBottom: 16 }}>
|
|
36
|
+
<div style={{ fontSize: 17, fontWeight: 700 }}>{p.id}</div>
|
|
37
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)', marginTop: 2 }}>
|
|
38
|
+
gen {p.generation} · {new Date(p.createdAt).toLocaleDateString()}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* Score Grid */}
|
|
43
|
+
<div className="grid-4" style={{ marginBottom: 16 }}>
|
|
44
|
+
{[
|
|
45
|
+
{ label: 'Composite', value: p.score, color: 'var(--purple)' },
|
|
46
|
+
{ label: 'Task', value: p.scores.taskCompletion, color: 'var(--green)' },
|
|
47
|
+
{ label: 'Alignment', value: p.scores.correctionAlignment, color: 'var(--blue)' },
|
|
48
|
+
{ label: 'Side-Effect Free', value: p.scores.sideEffectFree, color: 'var(--yellow)' },
|
|
49
|
+
].map(s => (
|
|
50
|
+
<div key={s.label} style={{
|
|
51
|
+
padding: '10px 12px',
|
|
52
|
+
background: 'var(--bg-section)',
|
|
53
|
+
borderRadius: 10,
|
|
54
|
+
}}>
|
|
55
|
+
<div style={{
|
|
56
|
+
fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase',
|
|
57
|
+
letterSpacing: 1, marginBottom: 4, fontWeight: 600,
|
|
58
|
+
}}>{s.label}</div>
|
|
59
|
+
<div style={{
|
|
60
|
+
fontSize: 22, fontWeight: 800, lineHeight: 1,
|
|
61
|
+
color: s.value >= 0.8 ? 'var(--green)' : s.value >= 0.6 ? 'var(--yellow)' : 'var(--red)',
|
|
62
|
+
}}>
|
|
63
|
+
{(s.value * 100).toFixed(0)}
|
|
64
|
+
</div>
|
|
65
|
+
<div className="score-track" style={{ height: 4, marginTop: 6 }}>
|
|
66
|
+
<div className="score-fill" style={{
|
|
67
|
+
width: `${s.value * 100}%`,
|
|
68
|
+
background: s.value >= 0.8 ? 'var(--green)' : s.value >= 0.6 ? 'var(--yellow)' : 'var(--red)',
|
|
69
|
+
}} />
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Description */}
|
|
76
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
|
|
77
|
+
{p.changesDescription.slice(0, 200)}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
|
|
83
|
+
{frontier.programs.length === 0 && (
|
|
84
|
+
<div className="card">
|
|
85
|
+
<div className="empty-state">
|
|
86
|
+
<div className="empty-state-title">Frontier empty</div>
|
|
87
|
+
<div className="empty-state-desc">Run <code>helixevo evolve</code> to populate the frontier</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Canaries */}
|
|
94
|
+
<div className="card">
|
|
95
|
+
<div className="card-body">
|
|
96
|
+
<div className="card-header-label">Active Canaries</div>
|
|
97
|
+
{canaries.entries.length > 0 ? (
|
|
98
|
+
canaries.entries.map((c, i) => {
|
|
99
|
+
const daysLeft = Math.max(0, Math.ceil((new Date(c.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))
|
|
100
|
+
return (
|
|
101
|
+
<div key={i} style={{
|
|
102
|
+
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
103
|
+
padding: '10px 0', borderBottom: i < canaries.entries.length - 1 ? '1px solid var(--border-subtle)' : 'none',
|
|
104
|
+
}}>
|
|
105
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
106
|
+
<div style={{
|
|
107
|
+
width: 8, height: 8, borderRadius: '50%',
|
|
108
|
+
background: daysLeft > 0 ? 'var(--yellow)' : 'var(--green)',
|
|
109
|
+
}} />
|
|
110
|
+
<span style={{ fontWeight: 500, fontSize: 13 }}>{c.skillSlug}</span>
|
|
111
|
+
<span style={{ color: 'var(--text-muted)', fontSize: 11 }}>{c.version}</span>
|
|
112
|
+
</div>
|
|
113
|
+
<span className={`badge ${daysLeft > 0 ? 'badge-yellow' : 'badge-green'}`}>
|
|
114
|
+
{daysLeft > 0 ? `${daysLeft}d remaining` : 'Ready to promote'}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
) : (
|
|
120
|
+
<div style={{ color: 'var(--text-dim)', fontSize: 13, textAlign: 'center', padding: 20 }}>
|
|
121
|
+
No active canaries
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|