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.
@@ -0,0 +1,68 @@
1
+ import { loadGraph, loadSkillContent, loadHistory, listProjects, getProjectSkills } from '@/lib/data'
2
+ import NetworkClient from './client'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export default function SkillNetworkPage() {
7
+ const graph = loadGraph()
8
+ const history = loadHistory()
9
+ const projects = listProjects()
10
+
11
+ const generalized = graph.nodes.filter(n =>
12
+ graph.edges.some(e => e.type === 'inherits' && e.from === n.id)
13
+ )
14
+ const evolved = graph.nodes.filter(n =>
15
+ n.generation > 0 && !generalized.some(g => g.id === n.id)
16
+ )
17
+ const original = graph.nodes.filter(n =>
18
+ n.generation === 0 && !generalized.some(g => g.id === n.id)
19
+ )
20
+
21
+ // Build evolution history per skill
22
+ const evolutionBySkill: Record<string, {
23
+ id: string; timestamp: string; action: string; description: string
24
+ outcome: string; outcomeReason: string; task: number; align: number; sideEffect: number
25
+ }[]> = {}
26
+ for (const iter of history.iterations) {
27
+ for (const p of iter.proposals) {
28
+ if (!evolutionBySkill[p.targetSkill]) evolutionBySkill[p.targetSkill] = []
29
+ evolutionBySkill[p.targetSkill].push({
30
+ id: p.id, timestamp: iter.timestamp, action: p.action,
31
+ description: p.description, outcome: p.outcome, outcomeReason: p.outcomeReason,
32
+ task: p.judges.taskCompletion.score, align: p.judges.correctionAlignment.score,
33
+ sideEffect: p.judges.sideEffectCheck.score,
34
+ })
35
+ }
36
+ }
37
+
38
+ // Load skill contents
39
+ const skillContents: Record<string, string> = {}
40
+ for (const n of graph.nodes) {
41
+ const raw = loadSkillContent(n.id)
42
+ if (raw) skillContents[n.id] = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/)?.[1]?.trim() ?? raw
43
+ }
44
+
45
+ // Build project data
46
+ const projectData = projects.map(p => ({ name: p, ...getProjectSkills(p) }))
47
+
48
+ return (
49
+ <NetworkClient
50
+ allNodes={graph.nodes}
51
+ generalized={generalized}
52
+ evolved={evolved}
53
+ original={original}
54
+ edges={graph.edges}
55
+ evolutionBySkill={evolutionBySkill}
56
+ skillContents={skillContents}
57
+ projects={projectData}
58
+ stats={{
59
+ nodes: graph.nodes.length,
60
+ edges: graph.edges.length,
61
+ clusters: graph.clusters.length,
62
+ generalized: generalized.length,
63
+ evolved: evolved.length,
64
+ original: original.length,
65
+ }}
66
+ />
67
+ )
68
+ }
@@ -0,0 +1,147 @@
1
+ import { getDashboardSummary, loadFrontier, loadHistory, loadGraph } from '@/lib/data'
2
+
3
+ export const dynamic = 'force-dynamic'
4
+
5
+ function StatCard({ value, label, color, sub, accent }: { value: string | number; label: string; color: string; sub?: string; accent?: string }) {
6
+ return (
7
+ <div className="stat-card">
8
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
9
+ <div>
10
+ <div className="stat-value" style={{ color }}>{value}</div>
11
+ <div className="stat-label">{label}</div>
12
+ {sub && <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>{sub}</div>}
13
+ </div>
14
+ {accent && (
15
+ <div style={{
16
+ width: 36, height: 36, borderRadius: 10,
17
+ background: `${color}12`, display: 'flex', alignItems: 'center', justifyContent: 'center',
18
+ color, fontSize: 16,
19
+ }}>{accent}</div>
20
+ )}
21
+ </div>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ export default function Overview() {
27
+ const s = getDashboardSummary()
28
+ const frontier = loadFrontier()
29
+ const history = loadHistory()
30
+ const graph = loadGraph()
31
+ const recent = history.iterations.slice(-5).reverse()
32
+
33
+ return (
34
+ <div>
35
+ <div className="page-header">
36
+ <h1 className="page-title">Dashboard</h1>
37
+ <p className="page-desc">
38
+ Overview of your self-evolving skill ecosystem
39
+ </p>
40
+ </div>
41
+
42
+ {/* Stats */}
43
+ <div className="grid-5" style={{ marginBottom: 28 }}>
44
+ <StatCard value={s.skills.total} label="Total Skills" color="var(--purple)" sub={`${s.skills.evolved} evolved`} accent="◆" />
45
+ <StatCard value={s.evolution.accepted} label="Accepted" color="var(--green)" sub={`${s.evolution.rejected} rejected`} accent="✓" />
46
+ <StatCard value={s.failures.unresolved} label="Unresolved" color="var(--yellow)" sub={`of ${s.failures.total} total`} accent="!" />
47
+ <StatCard value={s.buffer.discoveries} label="Discoveries" color="var(--blue)" sub={`${s.buffer.drafts} drafts`} accent="◎" />
48
+ <StatCard value={frontier.programs.length} label="Frontier" color="var(--text-secondary)" sub={`/${frontier.capacity} capacity`} accent="▲" />
49
+ </div>
50
+
51
+ <div className="grid-2">
52
+ {/* Frontier */}
53
+ <div className="card">
54
+ <div className="card-body">
55
+ <div className="card-header-label">Pareto Frontier</div>
56
+ {frontier.programs.map((p, i) => (
57
+ <div key={`${p.id}-${i}`} style={{
58
+ padding: '12px 0',
59
+ borderBottom: i < frontier.programs.length - 1 ? '1px solid var(--border-subtle)' : 'none',
60
+ }}>
61
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
62
+ <span style={{ fontSize: 13, fontWeight: 600 }}>{p.id}</span>
63
+ <span style={{
64
+ fontSize: 20, fontWeight: 800, lineHeight: 1,
65
+ color: p.score >= 0.8 ? 'var(--green)' : 'var(--yellow)',
66
+ }}>
67
+ {(p.score * 100).toFixed(0)}
68
+ </span>
69
+ </div>
70
+ <div style={{ display: 'flex', gap: 6, fontSize: 11 }}>
71
+ <span className="score-pill" style={{ color: 'var(--green)' }}>T:{(p.scores.taskCompletion * 10).toFixed(0)}</span>
72
+ <span className="score-pill" style={{ color: 'var(--blue)' }}>A:{(p.scores.correctionAlignment * 10).toFixed(0)}</span>
73
+ <span className="score-pill" style={{ color: 'var(--purple)' }}>S:{(p.scores.sideEffectFree * 10).toFixed(0)}</span>
74
+ </div>
75
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', marginTop: 4, lineHeight: 1.4 }}>
76
+ {p.changesDescription.slice(0, 80)}
77
+ </div>
78
+ </div>
79
+ ))}
80
+ </div>
81
+ </div>
82
+
83
+ {/* Recent Evolution */}
84
+ <div className="card">
85
+ <div className="card-body">
86
+ <div className="card-header-label">Recent Evolution</div>
87
+ {recent.map(iter => (
88
+ <div key={iter.id} style={{
89
+ padding: '10px 0',
90
+ borderBottom: '1px solid var(--border-subtle)',
91
+ }}>
92
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, marginBottom: 8 }}>
93
+ <span style={{ fontWeight: 600, color: 'var(--text)' }}>{iter.id}</span>
94
+ <span style={{ color: 'var(--text-muted)' }}>{new Date(iter.timestamp).toLocaleDateString()}</span>
95
+ </div>
96
+ {iter.proposals.map(p => (
97
+ <div key={p.id} style={{
98
+ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, marginBottom: 4,
99
+ padding: '4px 0',
100
+ }}>
101
+ <span style={{
102
+ width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
103
+ background: p.outcome === 'accepted' ? 'var(--green)' : 'var(--red)',
104
+ }} />
105
+ <span style={{ fontWeight: 500 }}>{p.targetSkill}</span>
106
+ <span className="badge badge-gray" style={{ fontSize: 9 }}>{p.action}</span>
107
+ </div>
108
+ ))}
109
+ </div>
110
+ ))}
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ {/* All Skills Summary */}
116
+ <div className="card" style={{ marginTop: 16 }}>
117
+ <div className="card-body">
118
+ <div className="card-header-label">All Skills</div>
119
+ <div className="grid-2" style={{ gap: 6 }}>
120
+ {graph.nodes.sort((a, b) => b.score - a.score).map(n => (
121
+ <div key={n.id} style={{
122
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
123
+ padding: '8px 12px', borderRadius: 8, background: 'var(--bg-section)',
124
+ }}>
125
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
126
+ <span style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{n.name}</span>
127
+ {n.generation > 0 && <span className="badge badge-green" style={{ fontSize: 9 }}>gen {n.generation}</span>}
128
+ </div>
129
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 90, flexShrink: 0 }}>
130
+ <div className="score-track" style={{ width: 50 }}>
131
+ <div className="score-fill" style={{
132
+ width: `${n.score * 100}%`,
133
+ background: n.score >= 0.8 ? 'var(--green)' : n.score >= 0.6 ? 'var(--yellow)' : 'var(--red)',
134
+ }} />
135
+ </div>
136
+ <span style={{ fontSize: 12, fontWeight: 600, color: n.score >= 0.8 ? 'var(--green)' : 'var(--yellow)' }}>
137
+ {(n.score * 100).toFixed(0)}
138
+ </span>
139
+ </div>
140
+ </div>
141
+ ))}
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ )
147
+ }
@@ -0,0 +1,124 @@
1
+ import { loadBuffer } from '@/lib/data'
2
+
3
+ export const dynamic = 'force-dynamic'
4
+
5
+ export default function ResearchPage() {
6
+ const buffer = loadBuffer()
7
+
8
+ return (
9
+ <div>
10
+ <div className="page-header">
11
+ <h1 className="page-title">Proactive Research</h1>
12
+ <p className="page-desc">
13
+ Web search discoveries and skill drafts from the knowledge buffer
14
+ </p>
15
+ </div>
16
+
17
+ {/* Summary Stats */}
18
+ <div className="grid-3" style={{ marginBottom: 28 }}>
19
+ <div className="stat-card">
20
+ <div className="stat-value" style={{ color: 'var(--blue)' }}>{buffer.discoveries.length}</div>
21
+ <div className="stat-label">Discoveries</div>
22
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>of 50 capacity</div>
23
+ </div>
24
+ <div className="stat-card">
25
+ <div className="stat-value" style={{ color: 'var(--yellow)' }}>{buffer.drafts.length}</div>
26
+ <div className="stat-label">Drafts</div>
27
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>of 10 capacity</div>
28
+ </div>
29
+ <div className="stat-card">
30
+ <div className="stat-value" style={{ color: 'var(--green)' }}>
31
+ {buffer.drafts.filter(d => d.passRate >= 0.67).length}
32
+ </div>
33
+ <div className="stat-label">Near-Pass Drafts</div>
34
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>≥67% pass rate</div>
35
+ </div>
36
+ </div>
37
+
38
+ <div className="grid-2">
39
+ {/* Discoveries */}
40
+ <div className="card">
41
+ <div className="card-body">
42
+ <div className="card-header-label">Discoveries ({buffer.discoveries.length}/50)</div>
43
+ {buffer.discoveries.length === 0 ? (
44
+ <div className="empty-state" style={{ padding: 32 }}>
45
+ <div className="empty-state-icon">
46
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
47
+ <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
48
+ </svg>
49
+ </div>
50
+ <div className="empty-state-title">No discoveries yet</div>
51
+ <div className="empty-state-desc">Run <code>helixevo research</code> to start proactive web research</div>
52
+ </div>
53
+ ) : (
54
+ buffer.discoveries.sort((a, b) => b.score - a.score).map(d => (
55
+ <div key={d.id} style={{
56
+ padding: '12px 0',
57
+ borderBottom: '1px solid var(--border-subtle)',
58
+ }}>
59
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
60
+ <span style={{ fontSize: 13, fontWeight: 600, lineHeight: 1.3 }}>{d.title}</span>
61
+ <span className="score-pill" style={{ color: 'var(--blue)', flexShrink: 0, marginLeft: 8 }}>
62
+ {(d.score * 100).toFixed(0)}%
63
+ </span>
64
+ </div>
65
+ <div style={{ fontSize: 11, color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: 6 }}>
66
+ {d.summary.slice(0, 140)}
67
+ </div>
68
+ <span className="badge badge-blue">{d.gap}</span>
69
+ </div>
70
+ ))
71
+ )}
72
+ </div>
73
+ </div>
74
+
75
+ {/* Drafts */}
76
+ <div className="card">
77
+ <div className="card-body">
78
+ <div className="card-header-label">Drafts ({buffer.drafts.length}/10)</div>
79
+ {buffer.drafts.length === 0 ? (
80
+ <div className="empty-state" style={{ padding: 32 }}>
81
+ <div className="empty-state-icon">
82
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
83
+ <path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
84
+ </svg>
85
+ </div>
86
+ <div className="empty-state-title">No drafts yet</div>
87
+ <div className="empty-state-desc">Near-pass proposals are automatically saved here for future iteration</div>
88
+ </div>
89
+ ) : (
90
+ buffer.drafts.sort((a, b) => b.avgScore - a.avgScore).map(d => (
91
+ <div key={d.id} style={{
92
+ padding: '12px 0',
93
+ borderBottom: '1px solid var(--border-subtle)',
94
+ }}>
95
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
96
+ <span style={{ fontSize: 13, fontWeight: 600 }}>{d.skillName}</span>
97
+ <span className={`badge ${d.avgScore >= 6 ? 'badge-green' : 'badge-yellow'}`}>
98
+ {d.avgScore.toFixed(1)}/10 · iter {d.iteration}
99
+ </span>
100
+ </div>
101
+ <div style={{ fontSize: 11, color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: 6 }}>
102
+ {d.hypothesis.slice(0, 140)}
103
+ </div>
104
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
105
+ <span style={{ fontSize: 11, color: 'var(--text-dim)' }}>Pass rate:</span>
106
+ <div className="score-track" style={{ width: 60, height: 4 }}>
107
+ <div className="score-fill" style={{
108
+ width: `${d.passRate * 100}%`,
109
+ background: d.passRate >= 0.67 ? 'var(--green)' : 'var(--yellow)',
110
+ }} />
111
+ </div>
112
+ <b style={{ fontSize: 11, color: d.passRate >= 0.67 ? 'var(--green)' : 'var(--yellow)' }}>
113
+ {(d.passRate * 100).toFixed(0)}%
114
+ </b>
115
+ </div>
116
+ </div>
117
+ ))
118
+ )}
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,192 @@
1
+ 'use client'
2
+
3
+ import { memo } from 'react'
4
+ import { Handle, Position } from '@xyflow/react'
5
+
6
+ interface SkillNodeData {
7
+ name: string
8
+ score: number
9
+ generation: number
10
+ category: 'generalized' | 'evolved' | 'original' | 'project'
11
+ layer: string
12
+ description: string
13
+ inheritsFrom: string[]
14
+ children: string[]
15
+ enhances: string[]
16
+ conflicts: string[]
17
+ evolutionSummary: string[]
18
+ tags: string[]
19
+ }
20
+
21
+ const CATEGORY_STYLES = {
22
+ generalized: {
23
+ border: '#c4b5fd',
24
+ bg: 'linear-gradient(180deg, #faf5ff 0%, #f5f0ff 100%)',
25
+ headerBg: '#ede9fe',
26
+ accent: '#7c3aed',
27
+ label: 'GENERALIZED',
28
+ },
29
+ evolved: {
30
+ border: '#86efac',
31
+ bg: 'linear-gradient(180deg, #f0fdf4 0%, #ecfdf5 100%)',
32
+ headerBg: '#dcfce7',
33
+ accent: '#16a34a',
34
+ label: 'EVOLVED',
35
+ },
36
+ original: {
37
+ border: '#d1d5db',
38
+ bg: 'linear-gradient(180deg, #fafafa 0%, #f5f5f5 100%)',
39
+ headerBg: '#f3f4f6',
40
+ accent: '#6b7280',
41
+ label: 'ORIGINAL',
42
+ },
43
+ project: {
44
+ border: '#fcd34d',
45
+ bg: 'linear-gradient(180deg, #fffbeb 0%, #fef3c7 100%)',
46
+ headerBg: '#fef3c7',
47
+ accent: '#b45309',
48
+ label: 'PROJECT',
49
+ },
50
+ }
51
+
52
+ function SkillFlowNode({ data }: { data: SkillNodeData }) {
53
+ const style = CATEGORY_STYLES[data.category]
54
+ const scorePercent = Math.round(data.score * 100)
55
+ const scoreColor = data.score >= 0.8 ? '#059669' : data.score >= 0.6 ? '#d97706' : '#dc2626'
56
+
57
+ return (
58
+ <div style={{
59
+ background: style.bg,
60
+ border: `1.5px solid ${style.border}`,
61
+ borderRadius: 14,
62
+ width: 280,
63
+ fontFamily: '-apple-system, BlinkMacSystemFont, SF Pro Display, system-ui, sans-serif',
64
+ boxShadow: '0 2px 8px rgba(0,0,0,0.06), 0 0px 1px rgba(0,0,0,0.04)',
65
+ overflow: 'hidden',
66
+ transition: 'box-shadow 0.2s ease',
67
+ }}>
68
+ <Handle type="target" position={Position.Top} style={{ background: style.border, width: 8, height: 8, border: '2px solid #fff' }} />
69
+ <Handle type="source" position={Position.Bottom} style={{ background: style.border, width: 8, height: 8, border: '2px solid #fff' }} />
70
+ <Handle type="target" position={Position.Left} id="left" style={{ background: style.border, width: 6, height: 6, border: '2px solid #fff' }} />
71
+ <Handle type="source" position={Position.Right} id="right" style={{ background: style.border, width: 6, height: 6, border: '2px solid #fff' }} />
72
+
73
+ {/* Header */}
74
+ <div style={{
75
+ padding: '10px 16px',
76
+ background: style.headerBg,
77
+ borderBottom: `1px solid ${style.border}40`,
78
+ display: 'flex',
79
+ justifyContent: 'space-between',
80
+ alignItems: 'flex-start',
81
+ }}>
82
+ <div style={{ flex: 1, minWidth: 0 }}>
83
+ <div style={{
84
+ fontSize: 9,
85
+ fontWeight: 700,
86
+ letterSpacing: 1.5,
87
+ color: style.accent,
88
+ textTransform: 'uppercase',
89
+ marginBottom: 3,
90
+ }}>
91
+ {style.label}
92
+ </div>
93
+ <div style={{
94
+ fontSize: 14,
95
+ fontWeight: 700,
96
+ color: '#1a1d2e',
97
+ lineHeight: 1.3,
98
+ overflow: 'hidden',
99
+ textOverflow: 'ellipsis',
100
+ whiteSpace: 'nowrap',
101
+ }}>
102
+ {data.name}
103
+ </div>
104
+ </div>
105
+ <div style={{
106
+ display: 'flex',
107
+ flexDirection: 'column',
108
+ alignItems: 'center',
109
+ marginLeft: 10,
110
+ flexShrink: 0,
111
+ }}>
112
+ <div style={{
113
+ fontSize: 20,
114
+ fontWeight: 800,
115
+ color: scoreColor,
116
+ lineHeight: 1,
117
+ }}>
118
+ {scorePercent}
119
+ </div>
120
+ <div style={{ fontSize: 8, color: '#9ca3af', marginTop: 1, fontWeight: 500 }}>SCORE</div>
121
+ </div>
122
+ </div>
123
+
124
+ {/* Body */}
125
+ <div style={{ padding: '10px 16px 12px' }}>
126
+ {/* Score Bar */}
127
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
128
+ <div style={{ flex: 1, height: 4, background: '#e5e7eb', borderRadius: 2, overflow: 'hidden' }}>
129
+ <div style={{ width: `${scorePercent}%`, height: '100%', background: `linear-gradient(90deg, ${style.accent}80, ${scoreColor})`, borderRadius: 2, transition: 'width 0.5s ease' }} />
130
+ </div>
131
+ {data.generation > 0 && (
132
+ <span style={{
133
+ fontSize: 9, fontWeight: 700, padding: '1px 6px', borderRadius: 4,
134
+ background: '#dcfce7', color: '#059669',
135
+ }}>gen {data.generation}</span>
136
+ )}
137
+ </div>
138
+
139
+ {/* Evolution Summary or Description */}
140
+ {data.evolutionSummary.length > 0 ? (
141
+ <div style={{ marginBottom: 8 }}>
142
+ {data.evolutionSummary.slice(0, 2).map((s, i) => (
143
+ <div key={i} style={{
144
+ fontSize: 10, color: '#4b5563', lineHeight: 1.4, marginBottom: 3,
145
+ paddingLeft: 8, borderLeft: `2px solid ${style.accent}40`,
146
+ }}>
147
+ {s.slice(0, 70)}{s.length > 70 ? '...' : ''}
148
+ </div>
149
+ ))}
150
+ </div>
151
+ ) : data.description ? (
152
+ <div style={{ fontSize: 10, color: '#6b7280', lineHeight: 1.4, marginBottom: 8 }}>
153
+ {data.description.slice(0, 90)}{data.description.length > 90 ? '...' : ''}
154
+ </div>
155
+ ) : null}
156
+
157
+ {/* Connection Indicators */}
158
+ {(data.inheritsFrom.length > 0 || data.children.length > 0 || data.enhances.length > 0 || data.conflicts.length > 0) && (
159
+ <div style={{ display: 'flex', gap: 6, marginBottom: 6, flexWrap: 'wrap' }}>
160
+ {data.inheritsFrom.length > 0 && (
161
+ <span style={{ fontSize: 9, color: '#818cf8', fontWeight: 500 }}>↑ {data.inheritsFrom.length}</span>
162
+ )}
163
+ {data.children.length > 0 && (
164
+ <span style={{ fontSize: 9, color: '#818cf8', fontWeight: 500 }}>↓ {data.children.length}</span>
165
+ )}
166
+ {data.enhances.length > 0 && (
167
+ <span style={{ fontSize: 9, color: '#22c55e', fontWeight: 500 }}>↔ {data.enhances.length}</span>
168
+ )}
169
+ {data.conflicts.length > 0 && (
170
+ <span style={{ fontSize: 9, color: '#ef4444', fontWeight: 500 }}>⚡ {data.conflicts.length}</span>
171
+ )}
172
+ </div>
173
+ )}
174
+
175
+ {/* Tags */}
176
+ {data.tags.length > 0 && (
177
+ <div style={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
178
+ {data.tags.slice(0, 4).map(t => (
179
+ <span key={t} style={{
180
+ fontSize: 9, padding: '1px 6px', borderRadius: 4,
181
+ background: `${style.accent}10`, color: style.accent, fontWeight: 500,
182
+ border: `1px solid ${style.accent}20`,
183
+ }}>{t}</span>
184
+ ))}
185
+ </div>
186
+ )}
187
+ </div>
188
+ </div>
189
+ )
190
+ }
191
+
192
+ export default memo(SkillFlowNode)
@@ -0,0 +1,146 @@
1
+ import { readFileSync, existsSync, readdirSync } from 'fs'
2
+ import { join } from 'path'
3
+ import { homedir } from 'os'
4
+
5
+ const SG_DIR = join(homedir(), '.helix')
6
+
7
+ function readJson<T>(filename: string, fallback: T): T {
8
+ const path = join(SG_DIR, filename)
9
+ if (!existsSync(path)) return fallback
10
+ return JSON.parse(readFileSync(path, 'utf-8'))
11
+ }
12
+
13
+ function readJsonl<T>(filename: string): T[] {
14
+ const path = join(SG_DIR, filename)
15
+ if (!existsSync(path)) return []
16
+ return readFileSync(path, 'utf-8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l))
17
+ }
18
+
19
+ // ─── Types ──────────────────────────────────────────────────────
20
+
21
+ export interface SkillNode {
22
+ id: string; name: string; layer: string; score: number
23
+ generation: number; status: string; tags: string[]
24
+ failureCount: number; lastEvolved: string; project?: string
25
+ }
26
+
27
+ export interface SkillEdge {
28
+ from: string; to: string
29
+ type: 'inherits' | 'depends' | 'enhances' | 'conflicts' | 'co-evolves'
30
+ strength: number; detectedBy: string
31
+ }
32
+
33
+ export interface SkillGraph {
34
+ updated: string; nodes: SkillNode[]; edges: SkillEdge[]
35
+ clusters: { id: string; name: string; skills: string[]; cohesion: number }[]
36
+ }
37
+
38
+ export interface Failure {
39
+ id: string; sessionId: string; timestamp: string; project: string | null
40
+ userRequest: string; agentAction: string; correction: string
41
+ correctionType: string; skillsActive: string[]; resolved?: boolean
42
+ }
43
+
44
+ export interface Frontier {
45
+ capacity: number; programs: {
46
+ id: string; generation: number; score: number
47
+ scores: { taskCompletion: number; correctionAlignment: number; sideEffectFree: number }
48
+ changesDescription: string; createdAt: string
49
+ }[]
50
+ }
51
+
52
+ export interface Proposal {
53
+ id: string; targetSkill: string; action: string; description: string
54
+ judges: { taskCompletion: { score: number }; correctionAlignment: { score: number }; sideEffectCheck: { score: number } }
55
+ consensus: boolean; outcome: string; outcomeReason: string
56
+ regressionResult: { total: number; passed: number; passRate: number }
57
+ }
58
+
59
+ export interface Iteration {
60
+ id: string; timestamp: string; trigger: string; failureCount: number
61
+ proposals: Proposal[]
62
+ failureClusters: { type: string; count: number }[]
63
+ }
64
+
65
+ export interface KnowledgeBuffer {
66
+ discoveries: { id: string; title: string; summary: string; score: number; gap: string; lastAccessed: string }[]
67
+ drafts: { id: string; skillName: string; avgScore: number; passRate: number; iteration: number; hypothesis: string }[]
68
+ }
69
+
70
+ export interface CanaryEntry {
71
+ skillSlug: string; version: string; deployedAt: string; expiresAt: string
72
+ }
73
+
74
+ // ─── Loaders ────────────────────────────────────────────────────
75
+
76
+ export function loadGraph(): SkillGraph {
77
+ return readJson<SkillGraph>('skill-graph.json', { updated: '', nodes: [], edges: [], clusters: [] })
78
+ }
79
+
80
+ export function loadFailures(): Failure[] {
81
+ return readJsonl<Failure>('failures.jsonl')
82
+ }
83
+
84
+ export function loadFrontier(): Frontier {
85
+ return readJson<Frontier>('frontier.json', { capacity: 5, programs: [] })
86
+ }
87
+
88
+ export function loadHistory(): { iterations: Iteration[] } {
89
+ return readJson<{ iterations: Iteration[] }>('evolution-history.json', { iterations: [] })
90
+ }
91
+
92
+ export function loadGoldenCases(): { id: string; skill: string; input: string }[] {
93
+ return readJsonl('golden-cases.jsonl')
94
+ }
95
+
96
+ export function loadBuffer(): KnowledgeBuffer {
97
+ return readJson<KnowledgeBuffer>('knowledge-buffer.json', { discoveries: [], drafts: [] })
98
+ }
99
+
100
+ export function loadCanaries(): { entries: CanaryEntry[] } {
101
+ return readJson<{ entries: CanaryEntry[] }>('canary-registry.json', { entries: [] })
102
+ }
103
+
104
+ export function loadSkillContent(slug: string): string {
105
+ const path = join(SG_DIR, 'general', slug, 'SKILL.md')
106
+ if (!existsSync(path)) return ''
107
+ return readFileSync(path, 'utf-8')
108
+ }
109
+
110
+ export function listProjects(): string[] {
111
+ const failures = loadFailures()
112
+ const projects = [...new Set(failures.map(f => f.project).filter(Boolean))] as string[]
113
+ return projects
114
+ }
115
+
116
+ export function getProjectSkills(project: string): { failures: Failure[]; skills: string[] } {
117
+ const failures = loadFailures().filter(f => f.project === project)
118
+ const skills = [...new Set(failures.flatMap(f => f.skillsActive))]
119
+ return { failures, skills }
120
+ }
121
+
122
+ export function getDashboardSummary() {
123
+ const graph = loadGraph()
124
+ const failures = loadFailures()
125
+ const frontier = loadFrontier()
126
+ const history = loadHistory()
127
+ const buffer = loadBuffer()
128
+ const canaries = loadCanaries()
129
+ const goldenCases = loadGoldenCases()
130
+
131
+ const evolved = graph.nodes.filter(n => n.generation > 0)
132
+ const totalProposals = history.iterations.flatMap(i => i.proposals)
133
+ const accepted = totalProposals.filter(p => p.outcome === 'accepted')
134
+ const rejected = totalProposals.filter(p => p.outcome === 'rejected')
135
+
136
+ return {
137
+ skills: { total: graph.nodes.length, evolved: evolved.length, original: graph.nodes.length - evolved.length },
138
+ graph: { nodes: graph.nodes.length, edges: graph.edges.length, clusters: graph.clusters.length },
139
+ failures: { total: failures.length, unresolved: failures.filter(f => !f.resolved).length },
140
+ frontier: { size: frontier.programs.length, capacity: frontier.capacity, topScore: frontier.programs[0]?.score ?? 0 },
141
+ evolution: { runs: history.iterations.length, accepted: accepted.length, rejected: rejected.length },
142
+ buffer: { discoveries: buffer.discoveries.length, drafts: buffer.drafts.length },
143
+ canaries: canaries.entries.length,
144
+ goldenCases: goldenCases.length,
145
+ }
146
+ }