qualia-framework 4.1.1 → 4.3.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/agents/builder.md +28 -0
- package/agents/research-synthesizer.md +7 -0
- package/bin/cli.js +142 -2
- package/bin/install.js +68 -1
- package/bin/knowledge-flush.js +164 -0
- package/bin/knowledge.js +317 -0
- package/docs/journey-demo.html +1008 -0
- package/docs/reviews/v4.1.0-audit.html +1488 -0
- package/docs/reviews/v4.1.0-audit.md +263 -0
- package/hooks/git-guardrails.js +167 -0
- package/hooks/stop-session-log.js +180 -0
- package/package.json +1 -1
- package/skills/qualia-debug/SKILL.md +1 -1
- package/skills/qualia-design/SKILL.md +15 -0
- package/skills/qualia-flush/SKILL.md +200 -0
- package/skills/qualia-learn/SKILL.md +47 -37
- package/skills/qualia-new/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +3 -2
- package/skills/qualia-postmortem/SKILL.md +238 -0
- package/skills/qualia-review/SKILL.md +3 -2
- package/skills/qualia-verify/SKILL.md +60 -0
- package/templates/knowledge/agents.md +71 -0
- package/templates/knowledge/index.md +47 -0
- package/tests/bin.test.sh +316 -9
- package/tests/hooks.test.sh +122 -0
- package/tests/runner.js +7 -2
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Qualia Framework — A Project's Journey</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #04060a;
|
|
13
|
+
--bg-2: #080c12;
|
|
14
|
+
--surface: #0d1218;
|
|
15
|
+
--surface-2: #131a22;
|
|
16
|
+
--line: #1c232c;
|
|
17
|
+
--line-strong: #2a333d;
|
|
18
|
+
--ink: #eaeff5;
|
|
19
|
+
--ink-soft: #b4bec9;
|
|
20
|
+
--ink-mute: #6e7886;
|
|
21
|
+
--brand: #00ced9;
|
|
22
|
+
--brand-2: #5de3eb;
|
|
23
|
+
--accent: #ffbf5e;
|
|
24
|
+
--violet: #a58bff;
|
|
25
|
+
--ok: #6ee7a8;
|
|
26
|
+
--ease-std: cubic-bezier(0.4, 0, 0.2, 1);
|
|
27
|
+
--ease-dec: cubic-bezier(0, 0, 0.2, 1);
|
|
28
|
+
--ease-sp: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
32
|
+
html, body { margin: 0; padding: 0; }
|
|
33
|
+
html { scroll-behavior: smooth; }
|
|
34
|
+
body {
|
|
35
|
+
font-family: "IBM Plex Sans", system-ui, sans-serif;
|
|
36
|
+
background: var(--bg);
|
|
37
|
+
color: var(--ink);
|
|
38
|
+
-webkit-font-smoothing: antialiased;
|
|
39
|
+
overflow-x: hidden;
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ambient */
|
|
44
|
+
body::before {
|
|
45
|
+
content: "";
|
|
46
|
+
position: fixed; inset: 0;
|
|
47
|
+
background:
|
|
48
|
+
radial-gradient(1400px 800px at 75% -15%, rgba(0, 206, 217, 0.18), transparent 55%),
|
|
49
|
+
radial-gradient(1000px 700px at 15% 115%, rgba(165, 139, 255, 0.08), transparent 60%);
|
|
50
|
+
pointer-events: none; z-index: 0;
|
|
51
|
+
}
|
|
52
|
+
body::after {
|
|
53
|
+
content: "";
|
|
54
|
+
position: fixed; inset: 0;
|
|
55
|
+
background-image:
|
|
56
|
+
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
|
57
|
+
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
|
|
58
|
+
background-size: 56px 56px;
|
|
59
|
+
mask-image: radial-gradient(ellipse at center, black 25%, transparent 85%);
|
|
60
|
+
pointer-events: none; z-index: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
h1, h2, h3 {
|
|
64
|
+
font-family: "Fraunces", Georgia, serif;
|
|
65
|
+
font-weight: 500;
|
|
66
|
+
letter-spacing: -0.02em;
|
|
67
|
+
line-height: 1.1;
|
|
68
|
+
margin: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
code.inline {
|
|
72
|
+
font-family: "JetBrains Mono", monospace;
|
|
73
|
+
background: var(--surface-2);
|
|
74
|
+
border: 1px solid var(--line);
|
|
75
|
+
padding: 0.1em 0.45em;
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
color: var(--brand);
|
|
78
|
+
font-size: 0.88em;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); }
|
|
82
|
+
|
|
83
|
+
/* layout */
|
|
84
|
+
.stage-wrap {
|
|
85
|
+
position: relative;
|
|
86
|
+
z-index: 1;
|
|
87
|
+
min-height: 100vh;
|
|
88
|
+
display: grid;
|
|
89
|
+
grid-template-rows: auto 1fr auto;
|
|
90
|
+
padding: clamp(1rem, 3vw, 2rem) clamp(1rem, 4vw, 3rem);
|
|
91
|
+
gap: clamp(1rem, 2vw, 1.5rem);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* HEADER BAR */
|
|
95
|
+
.top {
|
|
96
|
+
display: flex; align-items: center; justify-content: space-between; gap: 1rem;
|
|
97
|
+
flex-wrap: wrap;
|
|
98
|
+
}
|
|
99
|
+
.brand {
|
|
100
|
+
display: inline-flex; align-items: center; gap: 10px;
|
|
101
|
+
font-family: "Fraunces", serif; font-weight: 500;
|
|
102
|
+
color: var(--ink); text-decoration: none;
|
|
103
|
+
font-size: 1.05rem;
|
|
104
|
+
letter-spacing: -0.01em;
|
|
105
|
+
}
|
|
106
|
+
.brand-mark { color: var(--brand); display: grid; place-items: center; }
|
|
107
|
+
.project-chip {
|
|
108
|
+
display: inline-flex; align-items: center; gap: 0.6rem;
|
|
109
|
+
padding: 0.45rem 0.75rem;
|
|
110
|
+
background: var(--surface);
|
|
111
|
+
border: 1px solid var(--line);
|
|
112
|
+
border-radius: 999px;
|
|
113
|
+
font-family: "JetBrains Mono", monospace;
|
|
114
|
+
font-size: 0.76rem;
|
|
115
|
+
color: var(--ink-soft);
|
|
116
|
+
}
|
|
117
|
+
.project-chip .pulse {
|
|
118
|
+
width: 8px; height: 8px; border-radius: 999px;
|
|
119
|
+
background: var(--brand);
|
|
120
|
+
box-shadow: 0 0 0 0 rgba(0,206,217,0.6);
|
|
121
|
+
animation: livePulse 1.8s infinite;
|
|
122
|
+
}
|
|
123
|
+
@keyframes livePulse {
|
|
124
|
+
0% { box-shadow: 0 0 0 0 rgba(0,206,217,0.5); }
|
|
125
|
+
70% { box-shadow: 0 0 0 10px rgba(0,206,217,0); }
|
|
126
|
+
100% { box-shadow: 0 0 0 0 rgba(0,206,217,0); }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* MAIN STAGE */
|
|
130
|
+
.stage {
|
|
131
|
+
position: relative;
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-template-columns: 1fr;
|
|
134
|
+
grid-template-rows: auto 1fr;
|
|
135
|
+
gap: 1.5rem;
|
|
136
|
+
align-items: start;
|
|
137
|
+
}
|
|
138
|
+
@media (min-width: 1040px) {
|
|
139
|
+
.stage { grid-template-columns: minmax(0, 1.6fr) minmax(320px, 1fr); grid-template-rows: 1fr; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Title / narration column */
|
|
143
|
+
.narration {
|
|
144
|
+
display: flex; flex-direction: column; gap: 1rem;
|
|
145
|
+
}
|
|
146
|
+
.eyebrow {
|
|
147
|
+
font-family: "JetBrains Mono", monospace;
|
|
148
|
+
font-size: 0.72rem;
|
|
149
|
+
color: var(--brand);
|
|
150
|
+
letter-spacing: 0.22em;
|
|
151
|
+
text-transform: uppercase;
|
|
152
|
+
}
|
|
153
|
+
.eyebrow .chapter {
|
|
154
|
+
color: var(--ink-mute);
|
|
155
|
+
margin-left: 0.6rem;
|
|
156
|
+
letter-spacing: 0.1em;
|
|
157
|
+
}
|
|
158
|
+
h1.journey-title {
|
|
159
|
+
font-size: clamp(2rem, 1rem + 3.5vw, 4rem);
|
|
160
|
+
font-variation-settings: "opsz" 144;
|
|
161
|
+
}
|
|
162
|
+
h1.journey-title .accent {
|
|
163
|
+
color: var(--brand);
|
|
164
|
+
font-style: italic;
|
|
165
|
+
font-weight: 400;
|
|
166
|
+
}
|
|
167
|
+
.narration p {
|
|
168
|
+
color: var(--ink-soft);
|
|
169
|
+
font-size: clamp(0.95rem, 0.6rem + 0.4vw, 1.05rem);
|
|
170
|
+
max-width: 56ch;
|
|
171
|
+
line-height: 1.6;
|
|
172
|
+
margin: 0;
|
|
173
|
+
min-height: 3.2em;
|
|
174
|
+
}
|
|
175
|
+
.step-badge {
|
|
176
|
+
display: inline-flex; align-items: center; gap: 0.65rem;
|
|
177
|
+
padding: 0.4rem 0.75rem;
|
|
178
|
+
border: 1px solid var(--line);
|
|
179
|
+
background: var(--surface);
|
|
180
|
+
border-radius: 8px;
|
|
181
|
+
font-family: "JetBrains Mono", monospace;
|
|
182
|
+
font-size: 0.8rem;
|
|
183
|
+
color: var(--ink);
|
|
184
|
+
width: fit-content;
|
|
185
|
+
transition: border-color 300ms;
|
|
186
|
+
}
|
|
187
|
+
.step-badge.live { border-color: var(--brand); color: var(--brand); }
|
|
188
|
+
.step-badge .tag { color: var(--ink-mute); }
|
|
189
|
+
|
|
190
|
+
/* Terminal (right column) */
|
|
191
|
+
.term {
|
|
192
|
+
background: var(--surface);
|
|
193
|
+
border: 1px solid var(--line);
|
|
194
|
+
border-radius: 12px;
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
box-shadow: 0 40px 80px -50px rgba(0, 206, 217, 0.3);
|
|
197
|
+
display: flex; flex-direction: column;
|
|
198
|
+
min-height: 280px;
|
|
199
|
+
}
|
|
200
|
+
.term-head {
|
|
201
|
+
display: flex; align-items: center; gap: 0.4rem;
|
|
202
|
+
padding: 10px 14px;
|
|
203
|
+
background: var(--surface-2);
|
|
204
|
+
border-bottom: 1px solid var(--line);
|
|
205
|
+
font-family: "JetBrains Mono", monospace;
|
|
206
|
+
font-size: 0.74rem;
|
|
207
|
+
color: var(--ink-mute);
|
|
208
|
+
}
|
|
209
|
+
.term-dot { width: 9px; height: 9px; border-radius: 999px; }
|
|
210
|
+
.term-dot.r { background: #f87171; }
|
|
211
|
+
.term-dot.y { background: #ffbf5e; }
|
|
212
|
+
.term-dot.g { background: #6ee7a8; }
|
|
213
|
+
.term-title { margin-left: 0.45rem; }
|
|
214
|
+
.term-body {
|
|
215
|
+
padding: 1rem 1.2rem;
|
|
216
|
+
font-family: "JetBrains Mono", monospace;
|
|
217
|
+
font-size: 0.78rem;
|
|
218
|
+
line-height: 1.75;
|
|
219
|
+
color: var(--ink-soft);
|
|
220
|
+
flex: 1;
|
|
221
|
+
overflow: hidden;
|
|
222
|
+
position: relative;
|
|
223
|
+
}
|
|
224
|
+
.term-line { display: block; opacity: 0; transform: translateY(3px); }
|
|
225
|
+
.term-line.show { opacity: 1; transform: none; transition: opacity 280ms, transform 280ms; }
|
|
226
|
+
.t-prompt { color: var(--brand); }
|
|
227
|
+
.t-cmd { color: var(--ink); }
|
|
228
|
+
.t-ok { color: var(--ok); }
|
|
229
|
+
.t-warn { color: var(--accent); }
|
|
230
|
+
.t-mute { color: var(--ink-mute); }
|
|
231
|
+
.t-violet { color: var(--violet); }
|
|
232
|
+
.t-accent { color: var(--accent); }
|
|
233
|
+
|
|
234
|
+
/* JOURNEY TRACK (SVG) */
|
|
235
|
+
.track {
|
|
236
|
+
position: relative;
|
|
237
|
+
border: 1px solid var(--line);
|
|
238
|
+
border-radius: 20px;
|
|
239
|
+
background:
|
|
240
|
+
linear-gradient(180deg, var(--surface), var(--bg-2));
|
|
241
|
+
overflow: hidden;
|
|
242
|
+
}
|
|
243
|
+
.track svg {
|
|
244
|
+
display: block;
|
|
245
|
+
width: 100%;
|
|
246
|
+
height: clamp(320px, 44vh, 520px);
|
|
247
|
+
}
|
|
248
|
+
.track-footer {
|
|
249
|
+
border-top: 1px solid var(--line);
|
|
250
|
+
padding: 0.85rem 1.1rem;
|
|
251
|
+
display: flex; align-items: center; gap: 1rem;
|
|
252
|
+
flex-wrap: wrap;
|
|
253
|
+
}
|
|
254
|
+
.progress-label {
|
|
255
|
+
font-family: "JetBrains Mono", monospace;
|
|
256
|
+
font-size: 0.72rem;
|
|
257
|
+
color: var(--ink-mute);
|
|
258
|
+
letter-spacing: 0.12em;
|
|
259
|
+
text-transform: uppercase;
|
|
260
|
+
min-width: 90px;
|
|
261
|
+
}
|
|
262
|
+
.progress-bar {
|
|
263
|
+
flex: 1;
|
|
264
|
+
min-width: 160px;
|
|
265
|
+
height: 4px;
|
|
266
|
+
background: var(--line);
|
|
267
|
+
border-radius: 999px;
|
|
268
|
+
overflow: hidden;
|
|
269
|
+
}
|
|
270
|
+
.progress-fill {
|
|
271
|
+
height: 100%;
|
|
272
|
+
background: linear-gradient(90deg, var(--brand), var(--brand-2));
|
|
273
|
+
width: 0%;
|
|
274
|
+
transition: width 600ms var(--ease-std);
|
|
275
|
+
}
|
|
276
|
+
.controls { display: inline-flex; gap: 0.4rem; }
|
|
277
|
+
.btn {
|
|
278
|
+
background: var(--surface-2);
|
|
279
|
+
border: 1px solid var(--line);
|
|
280
|
+
color: var(--ink);
|
|
281
|
+
font: inherit;
|
|
282
|
+
font-family: "JetBrains Mono", monospace;
|
|
283
|
+
font-size: 0.76rem;
|
|
284
|
+
padding: 0.45rem 0.8rem;
|
|
285
|
+
border-radius: 6px;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
transition: border-color 150ms, color 150ms, transform 100ms;
|
|
288
|
+
}
|
|
289
|
+
.btn:hover { border-color: var(--brand); color: var(--brand); }
|
|
290
|
+
.btn:active { transform: scale(0.97); }
|
|
291
|
+
.btn[aria-pressed="true"] { border-color: var(--brand); color: var(--brand); background: rgba(0,206,217,0.08); }
|
|
292
|
+
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
|
|
293
|
+
|
|
294
|
+
/* BOTTOM PANEL */
|
|
295
|
+
.bottom {
|
|
296
|
+
display: grid;
|
|
297
|
+
grid-template-columns: 1fr;
|
|
298
|
+
gap: 1rem;
|
|
299
|
+
}
|
|
300
|
+
@media (min-width: 840px) { .bottom { grid-template-columns: repeat(4, 1fr); } }
|
|
301
|
+
.stat {
|
|
302
|
+
border: 1px solid var(--line);
|
|
303
|
+
background: var(--surface);
|
|
304
|
+
border-radius: 10px;
|
|
305
|
+
padding: 0.85rem 1rem;
|
|
306
|
+
min-height: 78px;
|
|
307
|
+
display: flex; flex-direction: column; gap: 0.25rem;
|
|
308
|
+
position: relative;
|
|
309
|
+
overflow: hidden;
|
|
310
|
+
transition: border-color 200ms;
|
|
311
|
+
}
|
|
312
|
+
.stat.live { border-color: var(--brand); }
|
|
313
|
+
.stat .k {
|
|
314
|
+
font-family: "JetBrains Mono", monospace;
|
|
315
|
+
font-size: 0.68rem;
|
|
316
|
+
color: var(--ink-mute);
|
|
317
|
+
letter-spacing: 0.14em;
|
|
318
|
+
text-transform: uppercase;
|
|
319
|
+
}
|
|
320
|
+
.stat .v {
|
|
321
|
+
font-family: "Fraunces", serif;
|
|
322
|
+
font-size: clamp(1.3rem, 0.7rem + 1vw, 1.75rem);
|
|
323
|
+
color: var(--ink);
|
|
324
|
+
}
|
|
325
|
+
.stat.live .v { color: var(--brand); }
|
|
326
|
+
.stat .hint {
|
|
327
|
+
font-family: "JetBrains Mono", monospace;
|
|
328
|
+
font-size: 0.72rem;
|
|
329
|
+
color: var(--ink-mute);
|
|
330
|
+
margin-top: auto;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* SVG stage elements */
|
|
334
|
+
.station .ring {
|
|
335
|
+
fill: none;
|
|
336
|
+
stroke: var(--line-strong);
|
|
337
|
+
stroke-width: 1.4;
|
|
338
|
+
transition: stroke 400ms, stroke-width 400ms;
|
|
339
|
+
}
|
|
340
|
+
.station .core {
|
|
341
|
+
fill: var(--surface-2);
|
|
342
|
+
stroke: var(--line-strong);
|
|
343
|
+
stroke-width: 1.5;
|
|
344
|
+
transition: fill 400ms, stroke 400ms;
|
|
345
|
+
}
|
|
346
|
+
.station.passed .ring { stroke: var(--ok); opacity: 0.5; }
|
|
347
|
+
.station.passed .core { fill: rgba(110,231,168,0.15); stroke: var(--ok); }
|
|
348
|
+
.station.active .ring { stroke: var(--brand); stroke-width: 2; }
|
|
349
|
+
.station.active .core { fill: rgba(0,206,217,0.2); stroke: var(--brand); }
|
|
350
|
+
.station .label {
|
|
351
|
+
font-family: "JetBrains Mono", monospace;
|
|
352
|
+
font-size: 10px;
|
|
353
|
+
fill: var(--ink-mute);
|
|
354
|
+
text-anchor: middle;
|
|
355
|
+
letter-spacing: 0.1em;
|
|
356
|
+
transition: fill 400ms;
|
|
357
|
+
}
|
|
358
|
+
.station.active .label { fill: var(--brand); }
|
|
359
|
+
.station.passed .label { fill: var(--ok); opacity: 0.7; }
|
|
360
|
+
.station .cmd {
|
|
361
|
+
font-family: "IBM Plex Sans", sans-serif;
|
|
362
|
+
font-size: 11px;
|
|
363
|
+
fill: var(--ink-soft);
|
|
364
|
+
text-anchor: middle;
|
|
365
|
+
font-weight: 500;
|
|
366
|
+
transition: fill 400ms;
|
|
367
|
+
}
|
|
368
|
+
.station.active .cmd { fill: var(--ink); }
|
|
369
|
+
|
|
370
|
+
/* floating agent orbs */
|
|
371
|
+
.orb {
|
|
372
|
+
transform-origin: center;
|
|
373
|
+
}
|
|
374
|
+
.orb .body {
|
|
375
|
+
fill: var(--surface-2);
|
|
376
|
+
stroke: var(--brand);
|
|
377
|
+
stroke-width: 1.4;
|
|
378
|
+
filter: drop-shadow(0 0 10px rgba(0,206,217,0.35));
|
|
379
|
+
}
|
|
380
|
+
.orb .lbl {
|
|
381
|
+
font-family: "JetBrains Mono", monospace;
|
|
382
|
+
font-size: 9px;
|
|
383
|
+
fill: var(--brand);
|
|
384
|
+
text-anchor: middle;
|
|
385
|
+
}
|
|
386
|
+
.orb-fade-in { animation: orbIn 500ms var(--ease-sp) both; }
|
|
387
|
+
.orb-fade-out { animation: orbOut 400ms var(--ease-std) both; }
|
|
388
|
+
@keyframes orbIn { from { opacity: 0; transform: scale(0.3); } to { opacity: 1; transform: scale(1); } }
|
|
389
|
+
@keyframes orbOut { to { opacity: 0; transform: scale(0.3); } }
|
|
390
|
+
|
|
391
|
+
/* footer */
|
|
392
|
+
footer {
|
|
393
|
+
position: relative; z-index: 1;
|
|
394
|
+
padding: 1.5rem clamp(1rem, 4vw, 3rem);
|
|
395
|
+
color: var(--ink-mute);
|
|
396
|
+
font-size: 0.82rem;
|
|
397
|
+
text-align: center;
|
|
398
|
+
border-top: 1px solid var(--line);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* reduced motion */
|
|
402
|
+
@media (prefers-reduced-motion: reduce) {
|
|
403
|
+
*, *::before, *::after {
|
|
404
|
+
animation-duration: 0.01ms !important;
|
|
405
|
+
animation-iteration-count: 1 !important;
|
|
406
|
+
transition-duration: 0.01ms !important;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
</style>
|
|
410
|
+
</head>
|
|
411
|
+
<body>
|
|
412
|
+
|
|
413
|
+
<div class="stage-wrap">
|
|
414
|
+
<!-- HEADER -->
|
|
415
|
+
<header class="top">
|
|
416
|
+
<a class="brand" href="#">
|
|
417
|
+
<span class="brand-mark" aria-hidden="true">
|
|
418
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M12 2 3 7v10l9 5 9-5V7z"/></svg>
|
|
419
|
+
</span>
|
|
420
|
+
Qualia Framework · a project's journey
|
|
421
|
+
</a>
|
|
422
|
+
<span class="project-chip" aria-live="polite">
|
|
423
|
+
<span class="pulse" aria-hidden="true"></span>
|
|
424
|
+
<span id="projectName">sakani-app</span>
|
|
425
|
+
<span style="color:var(--ink-mute)">·</span>
|
|
426
|
+
<span id="milestoneName">Foundation</span>
|
|
427
|
+
</span>
|
|
428
|
+
</header>
|
|
429
|
+
|
|
430
|
+
<!-- STAGE -->
|
|
431
|
+
<section class="stage">
|
|
432
|
+
<!-- Narration + Track -->
|
|
433
|
+
<div class="narration">
|
|
434
|
+
<p class="eyebrow">The Road <span class="chapter" id="chapter">Chapter 1 / 8</span></p>
|
|
435
|
+
<h1 class="journey-title"><span id="titleMain">Kickoff</span> <span class="accent" id="titleSub">— research & roadmap</span></h1>
|
|
436
|
+
<p id="narrationBody">A new client project begins with <code class="inline">/qualia-new</code>. Four researcher subagents spawn in parallel to study stack, features, architecture, and common pitfalls.</p>
|
|
437
|
+
<span class="step-badge live" id="stepBadge">
|
|
438
|
+
<span class="tag">step</span>
|
|
439
|
+
<span id="stepCmd">/qualia-new</span>
|
|
440
|
+
</span>
|
|
441
|
+
|
|
442
|
+
<!-- The main journey track (SVG) -->
|
|
443
|
+
<div class="track" aria-label="Journey animation">
|
|
444
|
+
<svg viewBox="0 0 1200 480" preserveAspectRatio="xMidYMid meet">
|
|
445
|
+
<defs>
|
|
446
|
+
<filter id="glow">
|
|
447
|
+
<feGaussianBlur stdDeviation="3.5" result="b"/>
|
|
448
|
+
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
|
|
449
|
+
</filter>
|
|
450
|
+
<radialGradient id="tokenGrad">
|
|
451
|
+
<stop offset="0" stop-color="#5de3eb" stop-opacity="1"/>
|
|
452
|
+
<stop offset="0.6" stop-color="#00ced9" stop-opacity="0.9"/>
|
|
453
|
+
<stop offset="1" stop-color="#00ced9" stop-opacity="0"/>
|
|
454
|
+
</radialGradient>
|
|
455
|
+
</defs>
|
|
456
|
+
|
|
457
|
+
<!-- the winding road -->
|
|
458
|
+
<path id="journeyPath"
|
|
459
|
+
d="M 80 240
|
|
460
|
+
C 200 80, 320 80, 430 240
|
|
461
|
+
S 660 400, 770 240
|
|
462
|
+
S 1000 80, 1120 240"
|
|
463
|
+
fill="none"
|
|
464
|
+
stroke="#1c232c"
|
|
465
|
+
stroke-width="3"
|
|
466
|
+
stroke-linecap="round"/>
|
|
467
|
+
|
|
468
|
+
<!-- completed highlight -->
|
|
469
|
+
<path id="journeyDone"
|
|
470
|
+
d="M 80 240
|
|
471
|
+
C 200 80, 320 80, 430 240
|
|
472
|
+
S 660 400, 770 240
|
|
473
|
+
S 1000 80, 1120 240"
|
|
474
|
+
fill="none"
|
|
475
|
+
stroke="#00ced9"
|
|
476
|
+
stroke-width="3"
|
|
477
|
+
stroke-linecap="round"
|
|
478
|
+
stroke-dasharray="0 3000"
|
|
479
|
+
opacity="0.9"/>
|
|
480
|
+
|
|
481
|
+
<!-- stations placed via JS -->
|
|
482
|
+
<g id="stationsGroup"></g>
|
|
483
|
+
|
|
484
|
+
<!-- orbs spawned at current station -->
|
|
485
|
+
<g id="orbLayer"></g>
|
|
486
|
+
|
|
487
|
+
<!-- particles along the path -->
|
|
488
|
+
<g id="particleLayer"></g>
|
|
489
|
+
|
|
490
|
+
<!-- the PROJECT TOKEN that travels -->
|
|
491
|
+
<g id="projectToken" transform="translate(80 240)">
|
|
492
|
+
<circle r="28" fill="url(#tokenGrad)" opacity="0.5"/>
|
|
493
|
+
<circle r="14" fill="#05070a" stroke="#00ced9" stroke-width="1.5" filter="url(#glow)"/>
|
|
494
|
+
<text y="4" text-anchor="middle" font-family="JetBrains Mono, monospace" font-size="9" fill="#00ced9" font-weight="600">PRJ</text>
|
|
495
|
+
</g>
|
|
496
|
+
</svg>
|
|
497
|
+
|
|
498
|
+
<div class="track-footer">
|
|
499
|
+
<span class="progress-label">Progress</span>
|
|
500
|
+
<div class="progress-bar" aria-hidden="true"><div class="progress-fill" id="progressFill"></div></div>
|
|
501
|
+
<span class="progress-label" id="progressPct">0%</span>
|
|
502
|
+
<span class="controls">
|
|
503
|
+
<button class="btn" id="playBtn" aria-pressed="true">Pause</button>
|
|
504
|
+
<button class="btn" id="restartBtn">Restart</button>
|
|
505
|
+
</span>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<!-- Live terminal -->
|
|
511
|
+
<aside class="term" aria-label="Live project trace">
|
|
512
|
+
<div class="term-head">
|
|
513
|
+
<span class="term-dot r"></span>
|
|
514
|
+
<span class="term-dot y"></span>
|
|
515
|
+
<span class="term-dot g"></span>
|
|
516
|
+
<span class="term-title">~/sakani-app · main</span>
|
|
517
|
+
</div>
|
|
518
|
+
<div class="term-body" id="termBody" role="log" aria-live="polite"></div>
|
|
519
|
+
</aside>
|
|
520
|
+
</section>
|
|
521
|
+
|
|
522
|
+
<!-- BOTTOM STATS -->
|
|
523
|
+
<section class="bottom" aria-label="Live stats">
|
|
524
|
+
<div class="stat" id="stat-phase">
|
|
525
|
+
<span class="k">Current Phase</span>
|
|
526
|
+
<span class="v" id="phaseV">—</span>
|
|
527
|
+
<span class="hint" id="phaseH">waiting to start</span>
|
|
528
|
+
</div>
|
|
529
|
+
<div class="stat" id="stat-agents">
|
|
530
|
+
<span class="k">Agents Active</span>
|
|
531
|
+
<span class="v" id="agentsV">0</span>
|
|
532
|
+
<span class="hint" id="agentsH">no subagents running</span>
|
|
533
|
+
</div>
|
|
534
|
+
<div class="stat" id="stat-tasks">
|
|
535
|
+
<span class="k">Tasks Completed</span>
|
|
536
|
+
<span class="v" id="tasksV">0 <span style="color:var(--ink-mute); font-size: 0.7em;">/ 4</span></span>
|
|
537
|
+
<span class="hint" id="tasksH">phase not yet started</span>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="stat" id="stat-gate">
|
|
540
|
+
<span class="k">Quality Gates</span>
|
|
541
|
+
<span class="v" id="gateV">—</span>
|
|
542
|
+
<span class="hint" id="gateH">pending</span>
|
|
543
|
+
</div>
|
|
544
|
+
</section>
|
|
545
|
+
</div>
|
|
546
|
+
|
|
547
|
+
<footer>
|
|
548
|
+
one project · one journey · fresh context per agent
|
|
549
|
+
</footer>
|
|
550
|
+
|
|
551
|
+
<script>
|
|
552
|
+
(() => {
|
|
553
|
+
'use strict';
|
|
554
|
+
|
|
555
|
+
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
556
|
+
const SVGNS = 'http://www.w3.org/2000/svg';
|
|
557
|
+
|
|
558
|
+
// ==========================================================
|
|
559
|
+
// Stations along the journey path (t = 0..1)
|
|
560
|
+
// ==========================================================
|
|
561
|
+
const stations = [
|
|
562
|
+
{ id: 'new', t: 0.03, label: 'KICKOFF', cmd: '/qualia-new', title: 'Kickoff', sub: '— research & roadmap' },
|
|
563
|
+
{ id: 'plan', t: 0.14, label: 'PLAN', cmd: '/qualia-plan', title: 'Plan', sub: '— phase plan + contracts' },
|
|
564
|
+
{ id: 'build', t: 0.30, label: 'BUILD', cmd: '/qualia-build', title: 'Build', sub: '— wave-based parallel work' },
|
|
565
|
+
{ id: 'verify', t: 0.46, label: 'VERIFY', cmd: '/qualia-verify', title: 'Verify', sub: '— goal-backward check' },
|
|
566
|
+
{ id: 'milestone', t: 0.59, label: 'MILESTONE', cmd: '/qualia-milestone', title: 'Milestone', sub: '— close & open next' },
|
|
567
|
+
{ id: 'polish', t: 0.72, label: 'POLISH', cmd: '/qualia-polish', title: 'Polish', sub: '— design & UX pass' },
|
|
568
|
+
{ id: 'ship', t: 0.86, label: 'SHIP', cmd: '/qualia-ship', title: 'Ship', sub: '— gates + deploy + verify' },
|
|
569
|
+
{ id: 'handoff', t: 0.98, label: 'HANDOFF', cmd: '/qualia-handoff', title: 'Handoff', sub: '— four deliverables' },
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
// ==========================================================
|
|
573
|
+
// Narration per chapter
|
|
574
|
+
// ==========================================================
|
|
575
|
+
const chapters = {
|
|
576
|
+
new: 'A new client project begins with `/qualia-new`. Four researcher subagents spawn in parallel — they study stack, features, architecture, and common pitfalls — then synthesise a JOURNEY.md.',
|
|
577
|
+
plan: 'For each phase, `/qualia-plan` runs a planner agent with a plan-checker in a revision loop. Tasks get specificity, waves, and a verification contract.',
|
|
578
|
+
build: 'Builders spawn one-per-task, in waves. Independent tasks run at the same time. Each builder gets only its task + PROJECT.md — a fresh context, every time.',
|
|
579
|
+
verify: 'A fresh verifier agent checks the phase against its goal — not just whether tasks ran, but whether the thing actually works.',
|
|
580
|
+
milestone: 'The milestone closes at a human gate. Artifacts archive, requirements flip to complete, and the next milestone opens from JOURNEY.md.',
|
|
581
|
+
polish: 'The last milestone is Handoff. First: polish. One sweep across responsive, accessible, motion, copy, and edge-case states.',
|
|
582
|
+
ship: 'Quality gates run: tsc, lint, build, tests. If all pass, the project deploys to production — then a 5-check post-deploy verification.',
|
|
583
|
+
handoff: 'Four deliverables: production URL, client documentation, credentials & assets archive, ERP finalization. The project is done.',
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// ==========================================================
|
|
587
|
+
// Terminal events per chapter
|
|
588
|
+
// ==========================================================
|
|
589
|
+
const termEvents = {
|
|
590
|
+
new: [
|
|
591
|
+
{ t: 100, html: '<span class="t-prompt">›</span> <span class="t-cmd">/qualia-new</span> <span class="t-mute">--auto</span>' },
|
|
592
|
+
{ t: 400, html: '<span class="t-mute">spawning 4 researchers in parallel…</span>' },
|
|
593
|
+
{ t: 750, html: '<span class="t-mute"> ├─ stack · Context7</span>' },
|
|
594
|
+
{ t: 900, html: '<span class="t-mute"> ├─ features · WebSearch</span>' },
|
|
595
|
+
{ t: 1050, html: '<span class="t-mute"> ├─ architecture</span>' },
|
|
596
|
+
{ t: 1200, html: '<span class="t-mute"> └─ pitfalls</span>' },
|
|
597
|
+
{ t: 2400, html: '<span class="t-ok">✓</span> research synthesised → <span class="t-accent">SUMMARY.md</span>' },
|
|
598
|
+
{ t: 2800, html: '<span class="t-violet">→</span> roadmapper · <span class="t-accent">JOURNEY.md</span> (5 milestones)' },
|
|
599
|
+
],
|
|
600
|
+
plan: [
|
|
601
|
+
{ t: 100, html: '<span class="t-prompt">›</span> <span class="t-cmd">/qualia-plan</span>' },
|
|
602
|
+
{ t: 400, html: '<span class="t-mute">planner: drafting 4 tasks, 2 waves…</span>' },
|
|
603
|
+
{ t: 1100, html: '<span class="t-mute">plan-checker: validating…</span>' },
|
|
604
|
+
{ t: 1800, html: '<span class="t-warn">✗</span> <span class="t-mute">task 3 missing verification contract</span>' },
|
|
605
|
+
{ t: 2200, html: '<span class="t-mute">revision 1/3 — planner revises task 3</span>' },
|
|
606
|
+
{ t: 3000, html: '<span class="t-ok">✓</span> plan validated · PHASE_PLAN.md' },
|
|
607
|
+
],
|
|
608
|
+
build: [
|
|
609
|
+
{ t: 100, html: '<span class="t-prompt">›</span> <span class="t-cmd">/qualia-build</span>' },
|
|
610
|
+
{ t: 400, html: '<span class="t-mute">wave 1 — 2 builders in parallel</span>' },
|
|
611
|
+
{ t: 900, html: '<span class="t-mute"> ⧗ task 1: auth schema + RLS</span>' },
|
|
612
|
+
{ t: 1050, html: '<span class="t-mute"> ⧗ task 2: login page + form</span>' },
|
|
613
|
+
{ t: 2100, html: ' <span class="t-ok">✓</span> task 1 committed <span class="t-mute">a1b2c3d</span>' },
|
|
614
|
+
{ t: 2300, html: ' <span class="t-ok">✓</span> task 2 committed <span class="t-mute">e4f5a6b</span>' },
|
|
615
|
+
{ t: 2600, html: '<span class="t-mute">wave 2 — 2 builders in parallel</span>' },
|
|
616
|
+
{ t: 3700, html: ' <span class="t-ok">✓</span> task 3 committed <span class="t-mute">7c8d9e0</span>' },
|
|
617
|
+
{ t: 3900, html: ' <span class="t-ok">✓</span> task 4 committed <span class="t-mute">f1e2d3c</span>' },
|
|
618
|
+
],
|
|
619
|
+
verify: [
|
|
620
|
+
{ t: 100, html: '<span class="t-prompt">›</span> <span class="t-cmd">/qualia-verify</span>' },
|
|
621
|
+
{ t: 500, html: '<span class="t-mute">verifier: loading success criteria…</span>' },
|
|
622
|
+
{ t: 1100, html: ' <span class="t-ok">✓</span> sign-in happy path' },
|
|
623
|
+
{ t: 1400, html: ' <span class="t-ok">✓</span> session persists across refresh' },
|
|
624
|
+
{ t: 1700, html: ' <span class="t-ok">✓</span> RLS denies unauthenticated reads' },
|
|
625
|
+
{ t: 2000, html: ' <span class="t-ok">✓</span> <span class="t-mute">npx tsc --noEmit clean</span>' },
|
|
626
|
+
{ t: 2400, html: '<span class="t-ok">✓</span> goal met — phase verified' },
|
|
627
|
+
],
|
|
628
|
+
milestone: [
|
|
629
|
+
{ t: 100, html: '<span class="t-prompt">›</span> <span class="t-cmd">/qualia-milestone</span>' },
|
|
630
|
+
{ t: 500, html: '<span class="t-mute">archiving artifacts of M1 · Foundation…</span>' },
|
|
631
|
+
{ t: 1100, html: '<span class="t-ok">✓</span> requirements marked complete' },
|
|
632
|
+
{ t: 1500, html: '<span class="t-violet">→</span> opening M2 · <span class="t-accent">Core Features</span>' },
|
|
633
|
+
],
|
|
634
|
+
polish: [
|
|
635
|
+
{ t: 100, html: '<span class="t-prompt">›</span> <span class="t-cmd">/qualia-polish</span>' },
|
|
636
|
+
{ t: 500, html: '<span class="t-mute">scanning: responsive, a11y, motion, copy…</span>' },
|
|
637
|
+
{ t: 1200, html: ' <span class="t-ok">✓</span> 44px touch targets' },
|
|
638
|
+
{ t: 1500, html: ' <span class="t-ok">✓</span> reduced-motion honored' },
|
|
639
|
+
{ t: 1800, html: ' <span class="t-ok">✓</span> empty / error / loading states' },
|
|
640
|
+
{ t: 2200, html: '<span class="t-ok">✓</span> polish complete' },
|
|
641
|
+
],
|
|
642
|
+
ship: [
|
|
643
|
+
{ t: 100, html: '<span class="t-prompt">›</span> <span class="t-cmd">/qualia-ship</span>' },
|
|
644
|
+
{ t: 500, html: '<span class="t-mute">gates:</span>' },
|
|
645
|
+
{ t: 800, html: ' <span class="t-ok">✓</span> tsc <span class="t-ok">✓</span> lint' },
|
|
646
|
+
{ t: 1050, html: ' <span class="t-ok">✓</span> build <span class="t-ok">✓</span> tests' },
|
|
647
|
+
{ t: 1400, html: '<span class="t-mute">vercel --prod …</span>' },
|
|
648
|
+
{ t: 2400, html: '<span class="t-ok">✓</span> live · <span class="t-accent">https://sakani.app</span>' },
|
|
649
|
+
{ t: 2700, html: '<span class="t-ok">✓</span> 5-check post-deploy verification' },
|
|
650
|
+
],
|
|
651
|
+
handoff: [
|
|
652
|
+
{ t: 100, html: '<span class="t-prompt">›</span> <span class="t-cmd">/qualia-handoff</span>' },
|
|
653
|
+
{ t: 500, html: ' <span class="t-ok">✓</span> production URL' },
|
|
654
|
+
{ t: 800, html: ' <span class="t-ok">✓</span> client documentation' },
|
|
655
|
+
{ t: 1100, html: ' <span class="t-ok">✓</span> credentials & assets' },
|
|
656
|
+
{ t: 1400, html: ' <span class="t-ok">✓</span> ERP finalization' },
|
|
657
|
+
{ t: 1800, html: '<span class="t-ok">✓</span> <span class="t-accent">project delivered.</span>' },
|
|
658
|
+
],
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// ==========================================================
|
|
662
|
+
// Per-chapter visual actions (orbs, particles, stats)
|
|
663
|
+
// ==========================================================
|
|
664
|
+
const chapterVisuals = {
|
|
665
|
+
new: {
|
|
666
|
+
milestone: 'Foundation',
|
|
667
|
+
phase: 'Kickoff',
|
|
668
|
+
phaseHint: 'research running',
|
|
669
|
+
gate: 'n/a',
|
|
670
|
+
gateHint: 'pre-plan',
|
|
671
|
+
orbs: [
|
|
672
|
+
{ name: 'stack', dx: -70, dy: -55 },
|
|
673
|
+
{ name: 'features', dx: 70, dy: -55 },
|
|
674
|
+
{ name: 'architecture', dx: -70, dy: 55 },
|
|
675
|
+
{ name: 'pitfalls', dx: 70, dy: 55 },
|
|
676
|
+
],
|
|
677
|
+
taskMax: 0, tasksDone: 0,
|
|
678
|
+
},
|
|
679
|
+
plan: {
|
|
680
|
+
milestone: 'Foundation',
|
|
681
|
+
phase: 'Phase 1 · Auth',
|
|
682
|
+
phaseHint: 'planning tasks',
|
|
683
|
+
gate: 'plan-checker',
|
|
684
|
+
gateHint: 'validating plan',
|
|
685
|
+
orbs: [
|
|
686
|
+
{ name: 'planner', dx: -55, dy: 0 },
|
|
687
|
+
{ name: 'plan-checker', dx: 55, dy: 0 },
|
|
688
|
+
],
|
|
689
|
+
taskMax: 4, tasksDone: 0,
|
|
690
|
+
},
|
|
691
|
+
build: {
|
|
692
|
+
milestone: 'Foundation',
|
|
693
|
+
phase: 'Phase 1 · Auth',
|
|
694
|
+
phaseHint: 'building in waves',
|
|
695
|
+
gate: 'frontend-guard',
|
|
696
|
+
gateHint: 'reading DESIGN.md',
|
|
697
|
+
orbs: [
|
|
698
|
+
{ name: 'build-1', dx: -90, dy: -45 },
|
|
699
|
+
{ name: 'build-2', dx: -30, dy: -45 },
|
|
700
|
+
{ name: 'build-3', dx: 30, dy: -45 },
|
|
701
|
+
{ name: 'build-4', dx: 90, dy: -45 },
|
|
702
|
+
],
|
|
703
|
+
taskMax: 4, tasksDone: 4, taskReveal: true,
|
|
704
|
+
},
|
|
705
|
+
verify: {
|
|
706
|
+
milestone: 'Foundation',
|
|
707
|
+
phase: 'Phase 1 · Auth',
|
|
708
|
+
phaseHint: 'goal-backward check',
|
|
709
|
+
gate: 'verification',
|
|
710
|
+
gateHint: 'spawning QA browser',
|
|
711
|
+
orbs: [
|
|
712
|
+
{ name: 'verifier', dx: -55, dy: 0 },
|
|
713
|
+
{ name: 'qa-browser', dx: 55, dy: 0 },
|
|
714
|
+
],
|
|
715
|
+
taskMax: 4, tasksDone: 4,
|
|
716
|
+
},
|
|
717
|
+
milestone: {
|
|
718
|
+
milestone: 'Core Features',
|
|
719
|
+
phase: 'Milestone boundary',
|
|
720
|
+
phaseHint: 'human gate — approved',
|
|
721
|
+
gate: 'gate passed',
|
|
722
|
+
gateHint: 'M1 → M2',
|
|
723
|
+
orbs: [],
|
|
724
|
+
taskMax: 0, tasksDone: 0,
|
|
725
|
+
},
|
|
726
|
+
polish: {
|
|
727
|
+
milestone: 'Handoff',
|
|
728
|
+
phase: 'Phase 1 · Polish',
|
|
729
|
+
phaseHint: 'design & UX sweep',
|
|
730
|
+
gate: 'design-guard',
|
|
731
|
+
gateHint: 'responsive + a11y',
|
|
732
|
+
orbs: [
|
|
733
|
+
{ name: 'polish-1', dx: -40, dy: 0 },
|
|
734
|
+
{ name: 'polish-2', dx: 40, dy: 0 },
|
|
735
|
+
],
|
|
736
|
+
taskMax: 3, tasksDone: 3,
|
|
737
|
+
},
|
|
738
|
+
ship: {
|
|
739
|
+
milestone: 'Handoff',
|
|
740
|
+
phase: 'Phase 3 · Ship',
|
|
741
|
+
phaseHint: 'deploying to prod',
|
|
742
|
+
gate: 'deploy-guard',
|
|
743
|
+
gateHint: 'tsc · lint · build · tests',
|
|
744
|
+
orbs: [
|
|
745
|
+
{ name: 'gates', dx: -55, dy: 0 },
|
|
746
|
+
{ name: 'deploy', dx: 55, dy: 0 },
|
|
747
|
+
],
|
|
748
|
+
taskMax: 5, tasksDone: 5,
|
|
749
|
+
},
|
|
750
|
+
handoff: {
|
|
751
|
+
milestone: 'Delivered',
|
|
752
|
+
phase: 'Complete',
|
|
753
|
+
phaseHint: 'project delivered',
|
|
754
|
+
gate: 'all passed',
|
|
755
|
+
gateHint: '4/4 deliverables',
|
|
756
|
+
orbs: [],
|
|
757
|
+
taskMax: 4, tasksDone: 4,
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// ==========================================================
|
|
762
|
+
// DOM + SVG setup
|
|
763
|
+
// ==========================================================
|
|
764
|
+
const journeyPath = document.getElementById('journeyPath');
|
|
765
|
+
const journeyDone = document.getElementById('journeyDone');
|
|
766
|
+
const totalLen = journeyPath.getTotalLength();
|
|
767
|
+
journeyDone.style.transition = 'stroke-dasharray 1200ms ' + 'cubic-bezier(0.4, 0, 0.2, 1)';
|
|
768
|
+
|
|
769
|
+
const stationsGroup = document.getElementById('stationsGroup');
|
|
770
|
+
stations.forEach((s, i) => {
|
|
771
|
+
const pt = journeyPath.getPointAtLength(totalLen * s.t);
|
|
772
|
+
const g = document.createElementNS(SVGNS, 'g');
|
|
773
|
+
g.setAttribute('class', 'station');
|
|
774
|
+
g.setAttribute('id', 'st-' + s.id);
|
|
775
|
+
g.setAttribute('transform', `translate(${pt.x} ${pt.y})`);
|
|
776
|
+
g.innerHTML = `
|
|
777
|
+
<circle class="ring" r="18"/>
|
|
778
|
+
<circle class="core" r="8"/>
|
|
779
|
+
<text class="label" y="-28">${s.label}</text>
|
|
780
|
+
<text class="cmd" y="38">${s.cmd}</text>
|
|
781
|
+
`;
|
|
782
|
+
stationsGroup.appendChild(g);
|
|
783
|
+
s._pt = pt;
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const projectToken = document.getElementById('projectToken');
|
|
787
|
+
const orbLayer = document.getElementById('orbLayer');
|
|
788
|
+
const particleLayer = document.getElementById('particleLayer');
|
|
789
|
+
|
|
790
|
+
// ==========================================================
|
|
791
|
+
// Movers / helpers
|
|
792
|
+
// ==========================================================
|
|
793
|
+
let tokenAnim = null;
|
|
794
|
+
function moveTokenTo(x, y, dur) {
|
|
795
|
+
if (tokenAnim) cancelAnimationFrame(tokenAnim);
|
|
796
|
+
const startTransform = projectToken.getAttribute('transform') || 'translate(80 240)';
|
|
797
|
+
const m = startTransform.match(/translate\(([-\d.]+)\s+([-\d.]+)\)/);
|
|
798
|
+
const sx = m ? parseFloat(m[1]) : 80;
|
|
799
|
+
const sy = m ? parseFloat(m[2]) : 240;
|
|
800
|
+
const start = performance.now();
|
|
801
|
+
const d = prefersReduced ? 0 : dur;
|
|
802
|
+
function tick(now) {
|
|
803
|
+
const k = Math.min(1, (now - start) / Math.max(d, 1));
|
|
804
|
+
const e = 1 - Math.pow(1 - k, 3); // easeOutCubic
|
|
805
|
+
const x1 = sx + (x - sx) * e;
|
|
806
|
+
const y1 = sy + (y - sy) * e;
|
|
807
|
+
projectToken.setAttribute('transform', `translate(${x1} ${y1})`);
|
|
808
|
+
if (k < 1) tokenAnim = requestAnimationFrame(tick);
|
|
809
|
+
}
|
|
810
|
+
tokenAnim = requestAnimationFrame(tick);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function clearOrbs() {
|
|
814
|
+
[...orbLayer.children].forEach(c => {
|
|
815
|
+
c.classList.add('orb-fade-out');
|
|
816
|
+
setTimeout(() => c.remove(), 400);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
function spawnOrbs(cx, cy, defs) {
|
|
820
|
+
defs.forEach((o, i) => {
|
|
821
|
+
setTimeout(() => {
|
|
822
|
+
const g = document.createElementNS(SVGNS, 'g');
|
|
823
|
+
g.setAttribute('class', 'orb orb-fade-in');
|
|
824
|
+
g.setAttribute('transform', `translate(${cx + o.dx} ${cy + o.dy})`);
|
|
825
|
+
g.innerHTML = `
|
|
826
|
+
<circle class="body" r="14"/>
|
|
827
|
+
<text class="lbl" y="3">${o.name}</text>
|
|
828
|
+
`;
|
|
829
|
+
orbLayer.appendChild(g);
|
|
830
|
+
}, i * (prefersReduced ? 0 : 130));
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function spawnParticles(fromX, fromY, count) {
|
|
835
|
+
if (prefersReduced) return;
|
|
836
|
+
for (let i = 0; i < count; i++) {
|
|
837
|
+
setTimeout(() => {
|
|
838
|
+
const c = document.createElementNS(SVGNS, 'circle');
|
|
839
|
+
c.setAttribute('r', 2.5);
|
|
840
|
+
c.setAttribute('cx', fromX);
|
|
841
|
+
c.setAttribute('cy', fromY);
|
|
842
|
+
c.setAttribute('fill', '#00ced9');
|
|
843
|
+
c.setAttribute('filter', 'url(#glow)');
|
|
844
|
+
particleLayer.appendChild(c);
|
|
845
|
+
const ang = Math.random() * Math.PI * 2;
|
|
846
|
+
const dist = 40 + Math.random() * 70;
|
|
847
|
+
const start = performance.now();
|
|
848
|
+
const dur = 900 + Math.random() * 500;
|
|
849
|
+
function tick(now) {
|
|
850
|
+
const k = (now - start) / dur;
|
|
851
|
+
if (k >= 1) { c.remove(); return; }
|
|
852
|
+
const e = 1 - Math.pow(1 - k, 2);
|
|
853
|
+
c.setAttribute('cx', fromX + Math.cos(ang) * dist * e);
|
|
854
|
+
c.setAttribute('cy', fromY + Math.sin(ang) * dist * e);
|
|
855
|
+
c.setAttribute('opacity', 1 - k);
|
|
856
|
+
requestAnimationFrame(tick);
|
|
857
|
+
}
|
|
858
|
+
requestAnimationFrame(tick);
|
|
859
|
+
}, i * 60);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function updateHighlight(t) {
|
|
864
|
+
const len = totalLen * t;
|
|
865
|
+
journeyDone.setAttribute('stroke-dasharray', `${len} ${totalLen}`);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ==========================================================
|
|
869
|
+
// Terminal
|
|
870
|
+
// ==========================================================
|
|
871
|
+
const termBody = document.getElementById('termBody');
|
|
872
|
+
let termTimers = [];
|
|
873
|
+
function termClear() {
|
|
874
|
+
termTimers.forEach(clearTimeout); termTimers = [];
|
|
875
|
+
termBody.innerHTML = '';
|
|
876
|
+
}
|
|
877
|
+
function termPush(html) {
|
|
878
|
+
const s = document.createElement('span');
|
|
879
|
+
s.className = 'term-line';
|
|
880
|
+
s.innerHTML = html;
|
|
881
|
+
termBody.appendChild(s);
|
|
882
|
+
requestAnimationFrame(() => s.classList.add('show'));
|
|
883
|
+
while (termBody.children.length > 11) termBody.firstChild.remove();
|
|
884
|
+
}
|
|
885
|
+
function termPlay(events) {
|
|
886
|
+
termClear();
|
|
887
|
+
events.forEach(e => {
|
|
888
|
+
termTimers.push(setTimeout(() => termPush(e.html), prefersReduced ? 0 : e.t));
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ==========================================================
|
|
893
|
+
// Stats panel
|
|
894
|
+
// ==========================================================
|
|
895
|
+
function setStats(v) {
|
|
896
|
+
document.getElementById('phaseV').textContent = v.phase;
|
|
897
|
+
document.getElementById('phaseH').textContent = v.phaseHint;
|
|
898
|
+
document.getElementById('agentsV').textContent = v.orbs.length;
|
|
899
|
+
document.getElementById('agentsH').textContent = v.orbs.length ? v.orbs.map(o => o.name).slice(0, 3).join(' · ') + (v.orbs.length > 3 ? ` +${v.orbs.length - 3}` : '') : 'no subagents running';
|
|
900
|
+
document.getElementById('tasksV').innerHTML = `${v.tasksDone} <span style="color:var(--ink-mute); font-size: 0.7em;">/ ${v.taskMax || '—'}</span>`;
|
|
901
|
+
document.getElementById('tasksH').textContent = v.taskMax ? (v.tasksDone === v.taskMax ? 'phase complete' : 'in progress') : 'no tasks yet';
|
|
902
|
+
document.getElementById('gateV').textContent = v.gate;
|
|
903
|
+
document.getElementById('gateH').textContent = v.gateHint;
|
|
904
|
+
|
|
905
|
+
[...document.querySelectorAll('.stat')].forEach(s => s.classList.remove('live'));
|
|
906
|
+
if (v.orbs.length) document.getElementById('stat-agents').classList.add('live');
|
|
907
|
+
if (v.taskMax && v.tasksDone < v.taskMax) document.getElementById('stat-tasks').classList.add('live');
|
|
908
|
+
document.getElementById('stat-phase').classList.add('live');
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ==========================================================
|
|
912
|
+
// Main journey loop
|
|
913
|
+
// ==========================================================
|
|
914
|
+
let currentIdx = 0;
|
|
915
|
+
let playing = true;
|
|
916
|
+
let chapterTimer = null;
|
|
917
|
+
|
|
918
|
+
function goToChapter(i) {
|
|
919
|
+
currentIdx = i;
|
|
920
|
+
const s = stations[i];
|
|
921
|
+
const v = chapterVisuals[s.id];
|
|
922
|
+
|
|
923
|
+
// station states
|
|
924
|
+
document.querySelectorAll('.station').forEach((el, idx) => {
|
|
925
|
+
el.classList.remove('active', 'passed');
|
|
926
|
+
if (idx < i) el.classList.add('passed');
|
|
927
|
+
if (idx === i) el.classList.add('active');
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// highlight + token
|
|
931
|
+
updateHighlight(s.t);
|
|
932
|
+
moveTokenTo(s._pt.x, s._pt.y, 1200);
|
|
933
|
+
|
|
934
|
+
// narration
|
|
935
|
+
document.getElementById('chapter').textContent = `Chapter ${i + 1} / ${stations.length}`;
|
|
936
|
+
document.getElementById('titleMain').textContent = s.title;
|
|
937
|
+
document.getElementById('titleSub').textContent = s.sub;
|
|
938
|
+
document.getElementById('narrationBody').innerHTML = chapters[s.id].replace(/`([^`]+)`/g, '<code class="inline">$1</code>');
|
|
939
|
+
document.getElementById('stepCmd').textContent = s.cmd;
|
|
940
|
+
|
|
941
|
+
// project chip milestone
|
|
942
|
+
document.getElementById('milestoneName').textContent = v.milestone;
|
|
943
|
+
|
|
944
|
+
// progress bar
|
|
945
|
+
const pct = Math.round(((i + 1) / stations.length) * 100);
|
|
946
|
+
document.getElementById('progressFill').style.width = pct + '%';
|
|
947
|
+
document.getElementById('progressPct').textContent = pct + '%';
|
|
948
|
+
|
|
949
|
+
// orbs + particles (after token arrives)
|
|
950
|
+
setTimeout(() => {
|
|
951
|
+
clearOrbs();
|
|
952
|
+
setTimeout(() => {
|
|
953
|
+
if (v.orbs.length) spawnOrbs(s._pt.x, s._pt.y, v.orbs);
|
|
954
|
+
spawnParticles(s._pt.x, s._pt.y, v.orbs.length ? 6 : 3);
|
|
955
|
+
}, 300);
|
|
956
|
+
}, prefersReduced ? 0 : 900);
|
|
957
|
+
|
|
958
|
+
// stats
|
|
959
|
+
setStats(v);
|
|
960
|
+
|
|
961
|
+
// terminal
|
|
962
|
+
termPlay(termEvents[s.id] || []);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function nextChapter() {
|
|
966
|
+
goToChapter((currentIdx + 1) % stations.length);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function startLoop() {
|
|
970
|
+
stopLoop();
|
|
971
|
+
// duration per chapter
|
|
972
|
+
chapterTimer = setInterval(nextChapter, 5200);
|
|
973
|
+
playing = true;
|
|
974
|
+
const btn = document.getElementById('playBtn');
|
|
975
|
+
btn.setAttribute('aria-pressed', 'true');
|
|
976
|
+
btn.textContent = 'Pause';
|
|
977
|
+
}
|
|
978
|
+
function stopLoop() {
|
|
979
|
+
if (chapterTimer) clearInterval(chapterTimer);
|
|
980
|
+
chapterTimer = null;
|
|
981
|
+
playing = false;
|
|
982
|
+
const btn = document.getElementById('playBtn');
|
|
983
|
+
btn.setAttribute('aria-pressed', 'false');
|
|
984
|
+
btn.textContent = 'Play';
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
document.getElementById('playBtn').addEventListener('click', () => {
|
|
988
|
+
if (playing) stopLoop(); else startLoop();
|
|
989
|
+
});
|
|
990
|
+
document.getElementById('restartBtn').addEventListener('click', () => {
|
|
991
|
+
stopLoop();
|
|
992
|
+
goToChapter(0);
|
|
993
|
+
startLoop();
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// kick it off
|
|
997
|
+
goToChapter(0);
|
|
998
|
+
if (prefersReduced) {
|
|
999
|
+
// play once to the end, no auto-loop
|
|
1000
|
+
stopLoop();
|
|
1001
|
+
} else {
|
|
1002
|
+
startLoop();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
})();
|
|
1006
|
+
</script>
|
|
1007
|
+
</body>
|
|
1008
|
+
</html>
|