htmlspec-kit 0.1.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/README.md +78 -0
- package/bin/htmlspec.ts +9 -0
- package/package.json +33 -0
- package/skills/htmlspec-kit/SKILL.md +584 -0
- package/skills/htmlspec-kit/agents/openai.yaml +4 -0
- package/skills/htmlspec-kit/assets/change-template.html +692 -0
- package/skills/htmlspec-kit/assets/spec.schema.json +155 -0
- package/src/cli.ts +422 -0
- package/src/html-document.ts +56 -0
- package/src/json-patch.ts +177 -0
- package/src/paths.ts +76 -0
- package/src/spec-data.ts +265 -0
- package/src/types.ts +106 -0
|
@@ -0,0 +1,692 @@
|
|
|
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>HTMLSpec Dossier</title>
|
|
7
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--ink: #171511;
|
|
11
|
+
--paper: #f3ead8;
|
|
12
|
+
--paper-2: #e5d5b8;
|
|
13
|
+
--line: rgba(23, 21, 17, 0.18);
|
|
14
|
+
--line-hard: rgba(23, 21, 17, 0.72);
|
|
15
|
+
--accent: #d2412e;
|
|
16
|
+
--accent-2: #0b5c6b;
|
|
17
|
+
--done: #2f7d4f;
|
|
18
|
+
--progress: #b66b00;
|
|
19
|
+
--todo: #6d6253;
|
|
20
|
+
--font-display: "Instrument Serif", Georgia, Cambria, serif;
|
|
21
|
+
--font-mono: "Berkeley Mono", "JetBrains Mono", "IBM Plex Mono", monospace;
|
|
22
|
+
--shadow: 0 24px 80px rgba(23, 21, 17, 0.23);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[x-cloak] {
|
|
26
|
+
display: none !important;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
* {
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
body {
|
|
34
|
+
margin: 0;
|
|
35
|
+
color: var(--ink);
|
|
36
|
+
font-family: var(--font-mono);
|
|
37
|
+
background:
|
|
38
|
+
linear-gradient(90deg, rgba(23, 21, 17, 0.055) 1px, transparent 1px) 0 0 / 28px 28px,
|
|
39
|
+
linear-gradient(rgba(23, 21, 17, 0.055) 1px, transparent 1px) 0 0 / 28px 28px,
|
|
40
|
+
radial-gradient(circle at 18% 10%, rgba(210, 65, 46, 0.22), transparent 30rem),
|
|
41
|
+
radial-gradient(circle at 88% 30%, rgba(11, 92, 107, 0.2), transparent 28rem),
|
|
42
|
+
var(--paper);
|
|
43
|
+
min-height: 100vh;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.sheet {
|
|
47
|
+
width: min(1180px, calc(100vw - 32px));
|
|
48
|
+
margin: 28px auto;
|
|
49
|
+
position: relative;
|
|
50
|
+
background: linear-gradient(135deg, rgba(255,255,255,0.34), rgba(255,255,255,0.08)), var(--paper);
|
|
51
|
+
border: 1px solid var(--line-hard);
|
|
52
|
+
box-shadow: var(--shadow);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.sheet::before,
|
|
56
|
+
.sheet::after {
|
|
57
|
+
content: "";
|
|
58
|
+
position: absolute;
|
|
59
|
+
width: 34px;
|
|
60
|
+
height: 34px;
|
|
61
|
+
border-color: var(--ink);
|
|
62
|
+
opacity: 0.88;
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.sheet::before {
|
|
67
|
+
left: 14px;
|
|
68
|
+
top: 14px;
|
|
69
|
+
border-left: 2px solid;
|
|
70
|
+
border-top: 2px solid;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.sheet::after {
|
|
74
|
+
right: 14px;
|
|
75
|
+
bottom: 14px;
|
|
76
|
+
border-right: 2px solid;
|
|
77
|
+
border-bottom: 2px solid;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
header {
|
|
81
|
+
display: grid;
|
|
82
|
+
grid-template-columns: 1fr auto;
|
|
83
|
+
gap: 24px;
|
|
84
|
+
padding: 44px 46px 28px;
|
|
85
|
+
border-bottom: 1px solid var(--line-hard);
|
|
86
|
+
background:
|
|
87
|
+
repeating-linear-gradient(-8deg, transparent 0 14px, rgba(23, 21, 17, 0.04) 14px 15px),
|
|
88
|
+
linear-gradient(90deg, rgba(210,65,46,0.14), transparent 45%);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.eyebrow,
|
|
92
|
+
.meta,
|
|
93
|
+
.stamp,
|
|
94
|
+
.task-id,
|
|
95
|
+
.label {
|
|
96
|
+
letter-spacing: 0.14em;
|
|
97
|
+
text-transform: uppercase;
|
|
98
|
+
font-size: 11px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.eyebrow {
|
|
102
|
+
color: var(--accent);
|
|
103
|
+
font-weight: 800;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
h1 {
|
|
107
|
+
margin: 10px 0 0;
|
|
108
|
+
font-family: var(--font-display);
|
|
109
|
+
font-size: clamp(48px, 9vw, 118px);
|
|
110
|
+
line-height: 0.82;
|
|
111
|
+
letter-spacing: -0.07em;
|
|
112
|
+
max-width: 800px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.stamp {
|
|
116
|
+
align-self: start;
|
|
117
|
+
border: 2px solid var(--accent);
|
|
118
|
+
color: var(--accent);
|
|
119
|
+
transform: rotate(3deg);
|
|
120
|
+
padding: 12px 16px;
|
|
121
|
+
font-weight: 900;
|
|
122
|
+
text-align: center;
|
|
123
|
+
min-width: 150px;
|
|
124
|
+
background: rgba(243, 234, 216, 0.72);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.meta-grid {
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-columns: repeat(4, 1fr);
|
|
130
|
+
border-bottom: 1px solid var(--line-hard);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.meta {
|
|
134
|
+
min-height: 78px;
|
|
135
|
+
padding: 18px 22px;
|
|
136
|
+
border-right: 1px solid var(--line);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.meta:last-child {
|
|
140
|
+
border-right: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.meta strong {
|
|
144
|
+
display: block;
|
|
145
|
+
margin-top: 9px;
|
|
146
|
+
font-size: 14px;
|
|
147
|
+
letter-spacing: 0;
|
|
148
|
+
text-transform: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
main {
|
|
152
|
+
display: grid;
|
|
153
|
+
grid-template-columns: minmax(0, 1fr) 360px;
|
|
154
|
+
gap: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.content {
|
|
158
|
+
border-right: 1px solid var(--line-hard);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
section,
|
|
162
|
+
aside {
|
|
163
|
+
padding: 34px 38px;
|
|
164
|
+
border-bottom: 1px solid var(--line-hard);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
section:last-child,
|
|
168
|
+
aside:last-child {
|
|
169
|
+
border-bottom: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
h2 {
|
|
173
|
+
margin: 0 0 20px;
|
|
174
|
+
font-family: var(--font-display);
|
|
175
|
+
font-size: clamp(32px, 5vw, 58px);
|
|
176
|
+
line-height: 0.9;
|
|
177
|
+
letter-spacing: -0.04em;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
h3 {
|
|
181
|
+
margin: 24px 0 10px;
|
|
182
|
+
font-size: 14px;
|
|
183
|
+
letter-spacing: 0.1em;
|
|
184
|
+
text-transform: uppercase;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
p,
|
|
188
|
+
li {
|
|
189
|
+
line-height: 1.6;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.summary {
|
|
193
|
+
font-size: 18px;
|
|
194
|
+
max-width: 78ch;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.cards {
|
|
198
|
+
display: grid;
|
|
199
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
200
|
+
gap: 16px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.card,
|
|
204
|
+
.requirement,
|
|
205
|
+
.decision,
|
|
206
|
+
.task-group {
|
|
207
|
+
border: 1px solid var(--line-hard);
|
|
208
|
+
background: rgba(255,255,255,0.22);
|
|
209
|
+
padding: 18px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.requirement {
|
|
213
|
+
margin-bottom: 16px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.requirement-title {
|
|
217
|
+
display: flex;
|
|
218
|
+
gap: 12px;
|
|
219
|
+
align-items: baseline;
|
|
220
|
+
margin-bottom: 10px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.req-id,
|
|
224
|
+
.task-id {
|
|
225
|
+
color: var(--accent-2);
|
|
226
|
+
font-weight: 900;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.scenario {
|
|
230
|
+
border-left: 3px solid var(--accent);
|
|
231
|
+
margin-top: 14px;
|
|
232
|
+
padding-left: 14px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.scenario dl {
|
|
236
|
+
display: grid;
|
|
237
|
+
grid-template-columns: 72px 1fr;
|
|
238
|
+
gap: 8px 12px;
|
|
239
|
+
margin: 8px 0 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.scenario dt {
|
|
243
|
+
font-weight: 900;
|
|
244
|
+
color: var(--accent);
|
|
245
|
+
text-transform: uppercase;
|
|
246
|
+
font-size: 11px;
|
|
247
|
+
letter-spacing: 0.12em;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.scenario dd {
|
|
251
|
+
margin: 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.rail {
|
|
255
|
+
background:
|
|
256
|
+
linear-gradient(rgba(23, 21, 17, 0.04), rgba(23, 21, 17, 0.04)),
|
|
257
|
+
rgba(229, 213, 184, 0.7);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.task-group {
|
|
261
|
+
margin-bottom: 16px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.task {
|
|
265
|
+
display: grid;
|
|
266
|
+
grid-template-columns: 64px 1fr auto;
|
|
267
|
+
gap: 10px;
|
|
268
|
+
align-items: start;
|
|
269
|
+
padding: 12px 0;
|
|
270
|
+
border-top: 1px dashed var(--line-hard);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.task:first-of-type {
|
|
274
|
+
border-top: 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.pill {
|
|
278
|
+
display: inline-block;
|
|
279
|
+
border: 1px solid currentColor;
|
|
280
|
+
padding: 4px 7px;
|
|
281
|
+
font-size: 10px;
|
|
282
|
+
letter-spacing: 0.1em;
|
|
283
|
+
text-transform: uppercase;
|
|
284
|
+
white-space: nowrap;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.status-done {
|
|
288
|
+
color: var(--done);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.status-in-progress {
|
|
292
|
+
color: var(--progress);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.status-todo {
|
|
296
|
+
color: var(--todo);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.meter {
|
|
300
|
+
height: 12px;
|
|
301
|
+
border: 1px solid var(--line-hard);
|
|
302
|
+
background: rgba(255,255,255,0.28);
|
|
303
|
+
margin: 14px 0 8px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.meter span {
|
|
307
|
+
display: block;
|
|
308
|
+
height: 100%;
|
|
309
|
+
background: repeating-linear-gradient(90deg, var(--accent-2) 0 8px, #0f778a 8px 12px);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.history {
|
|
313
|
+
margin: 0;
|
|
314
|
+
padding-left: 18px;
|
|
315
|
+
font-size: 12px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@media (max-width: 860px) {
|
|
319
|
+
header,
|
|
320
|
+
main,
|
|
321
|
+
.meta-grid,
|
|
322
|
+
.cards {
|
|
323
|
+
grid-template-columns: 1fr;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.content {
|
|
327
|
+
border-right: 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
header,
|
|
331
|
+
section,
|
|
332
|
+
aside {
|
|
333
|
+
padding-left: 24px;
|
|
334
|
+
padding-right: 24px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.stamp {
|
|
338
|
+
transform: none;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
</style>
|
|
342
|
+
</head>
|
|
343
|
+
<body>
|
|
344
|
+
<div class="sheet" x-data="htmlspec()" x-init="load()" x-cloak>
|
|
345
|
+
<header>
|
|
346
|
+
<div>
|
|
347
|
+
<div class="eyebrow">HTMLSpec / executable dossier</div>
|
|
348
|
+
<h1 x-text="data.title"></h1>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="stamp">
|
|
351
|
+
<div>SPEC</div>
|
|
352
|
+
<div x-text="data.status"></div>
|
|
353
|
+
</div>
|
|
354
|
+
</header>
|
|
355
|
+
|
|
356
|
+
<div class="meta-grid">
|
|
357
|
+
<div class="meta">Change ID<strong x-text="data.id"></strong></div>
|
|
358
|
+
<div class="meta">Owner<strong x-text="data.owner"></strong></div>
|
|
359
|
+
<div class="meta">Created<strong x-text="shortDate(data.createdAt)"></strong></div>
|
|
360
|
+
<div class="meta">Updated<strong x-text="shortDate(data.updatedAt)"></strong></div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<main>
|
|
364
|
+
<div class="content">
|
|
365
|
+
<section>
|
|
366
|
+
<div class="label">Summary</div>
|
|
367
|
+
<div class="summary" x-html="data.summaryHtml"></div>
|
|
368
|
+
</section>
|
|
369
|
+
|
|
370
|
+
<section>
|
|
371
|
+
<h2>Proposal</h2>
|
|
372
|
+
<h3>Why</h3>
|
|
373
|
+
<div x-html="data.proposal.whyHtml"></div>
|
|
374
|
+
<div class="cards">
|
|
375
|
+
<div class="card">
|
|
376
|
+
<h3>What Changes</h3>
|
|
377
|
+
<ul>
|
|
378
|
+
<template x-for="change in data.proposal.whatChanges" :key="change">
|
|
379
|
+
<li x-text="change"></li>
|
|
380
|
+
</template>
|
|
381
|
+
</ul>
|
|
382
|
+
</div>
|
|
383
|
+
<div class="card">
|
|
384
|
+
<h3>Impact</h3>
|
|
385
|
+
<ul>
|
|
386
|
+
<template x-for="impact in data.proposal.impact" :key="impact">
|
|
387
|
+
<li x-text="impact"></li>
|
|
388
|
+
</template>
|
|
389
|
+
</ul>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
<div class="cards">
|
|
393
|
+
<div class="card">
|
|
394
|
+
<h3>New Capabilities</h3>
|
|
395
|
+
<template x-for="capability in data.proposal.capabilities.new" :key="capability.name">
|
|
396
|
+
<p><strong x-text="capability.name"></strong>: <span x-text="capability.description"></span></p>
|
|
397
|
+
</template>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="card">
|
|
400
|
+
<h3>Modified Capabilities</h3>
|
|
401
|
+
<template x-for="capability in data.proposal.capabilities.modified" :key="capability.name">
|
|
402
|
+
<p><strong x-text="capability.name"></strong>: <span x-text="capability.description"></span></p>
|
|
403
|
+
</template>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</section>
|
|
407
|
+
|
|
408
|
+
<section>
|
|
409
|
+
<h2>Spec</h2>
|
|
410
|
+
<div x-html="data.spec.overviewHtml"></div>
|
|
411
|
+
<template x-for="operation in data.spec.operations" :key="operation.type">
|
|
412
|
+
<div>
|
|
413
|
+
<h3><span x-text="operation.type"></span> Requirements</h3>
|
|
414
|
+
<template x-for="req in operation.requirements" :key="operation.type + req.id">
|
|
415
|
+
<article class="requirement">
|
|
416
|
+
<div class="requirement-title">
|
|
417
|
+
<span class="req-id" x-text="req.id"></span>
|
|
418
|
+
<h3 x-text="req.title"></h3>
|
|
419
|
+
</div>
|
|
420
|
+
<div x-html="req.bodyHtml"></div>
|
|
421
|
+
<template x-if="req.reasonHtml">
|
|
422
|
+
<div x-html="req.reasonHtml"></div>
|
|
423
|
+
</template>
|
|
424
|
+
<template x-if="req.migrationHtml">
|
|
425
|
+
<div x-html="req.migrationHtml"></div>
|
|
426
|
+
</template>
|
|
427
|
+
<template x-for="scenario in req.scenarios" :key="scenario.id">
|
|
428
|
+
<div class="scenario">
|
|
429
|
+
<strong><span x-text="scenario.id"></span> / <span x-text="scenario.title"></span></strong>
|
|
430
|
+
<dl>
|
|
431
|
+
<template x-if="scenario.given">
|
|
432
|
+
<dt>Given</dt>
|
|
433
|
+
</template>
|
|
434
|
+
<template x-if="scenario.given">
|
|
435
|
+
<dd x-text="scenario.given"></dd>
|
|
436
|
+
</template>
|
|
437
|
+
<dt>When</dt>
|
|
438
|
+
<dd x-text="scenario.when"></dd>
|
|
439
|
+
<dt>Then</dt>
|
|
440
|
+
<dd x-text="scenario.then"></dd>
|
|
441
|
+
</dl>
|
|
442
|
+
</div>
|
|
443
|
+
</template>
|
|
444
|
+
</article>
|
|
445
|
+
</template>
|
|
446
|
+
</div>
|
|
447
|
+
</template>
|
|
448
|
+
</section>
|
|
449
|
+
|
|
450
|
+
<section>
|
|
451
|
+
<h2>Design</h2>
|
|
452
|
+
<h3>Context</h3>
|
|
453
|
+
<div x-html="data.design.contextHtml"></div>
|
|
454
|
+
<div class="cards">
|
|
455
|
+
<div class="card">
|
|
456
|
+
<h3>Goals</h3>
|
|
457
|
+
<ul>
|
|
458
|
+
<template x-for="goal in data.design.goals" :key="goal">
|
|
459
|
+
<li x-text="goal"></li>
|
|
460
|
+
</template>
|
|
461
|
+
</ul>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="card">
|
|
464
|
+
<h3>Non-Goals</h3>
|
|
465
|
+
<ul>
|
|
466
|
+
<template x-for="goal in data.design.nonGoals" :key="goal">
|
|
467
|
+
<li x-text="goal"></li>
|
|
468
|
+
</template>
|
|
469
|
+
</ul>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
<template x-for="decision in data.design.decisions" :key="decision.id">
|
|
473
|
+
<article class="decision">
|
|
474
|
+
<h3 x-text="decision.title"></h3>
|
|
475
|
+
<div x-html="decision.decisionHtml"></div>
|
|
476
|
+
<div x-html="decision.rationaleHtml"></div>
|
|
477
|
+
</article>
|
|
478
|
+
</template>
|
|
479
|
+
</section>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
<div class="rail">
|
|
483
|
+
<aside>
|
|
484
|
+
<h2>Tasks</h2>
|
|
485
|
+
<div class="meter"><span :style="`width: ${completion()}%`"></span></div>
|
|
486
|
+
<p><strong x-text="doneCount()"></strong> of <strong x-text="taskCount()"></strong> complete</p>
|
|
487
|
+
<template x-for="group in data.tasks" :key="group.id">
|
|
488
|
+
<div class="task-group">
|
|
489
|
+
<h3><span x-text="group.id"></span>. <span x-text="group.title"></span></h3>
|
|
490
|
+
<template x-for="task in group.items" :key="task.id">
|
|
491
|
+
<div class="task">
|
|
492
|
+
<span class="task-id" x-text="task.id"></span>
|
|
493
|
+
<span x-text="task.title"></span>
|
|
494
|
+
<span class="pill" :class="statusClass(task.status)" x-text="task.status"></span>
|
|
495
|
+
</div>
|
|
496
|
+
</template>
|
|
497
|
+
</div>
|
|
498
|
+
</template>
|
|
499
|
+
</aside>
|
|
500
|
+
|
|
501
|
+
<aside>
|
|
502
|
+
<h2>Risks</h2>
|
|
503
|
+
<ul>
|
|
504
|
+
<template x-for="risk in data.design.risksTradeoffs" :key="risk">
|
|
505
|
+
<li x-text="risk"></li>
|
|
506
|
+
</template>
|
|
507
|
+
</ul>
|
|
508
|
+
</aside>
|
|
509
|
+
|
|
510
|
+
<aside>
|
|
511
|
+
<h2>Migration</h2>
|
|
512
|
+
<ul>
|
|
513
|
+
<template x-for="step in data.design.migrationPlan" :key="step">
|
|
514
|
+
<li x-text="step"></li>
|
|
515
|
+
</template>
|
|
516
|
+
</ul>
|
|
517
|
+
</aside>
|
|
518
|
+
|
|
519
|
+
<aside>
|
|
520
|
+
<h2>Open</h2>
|
|
521
|
+
<ul>
|
|
522
|
+
<template x-for="question in data.design.openQuestions" :key="question">
|
|
523
|
+
<li x-text="question"></li>
|
|
524
|
+
</template>
|
|
525
|
+
</ul>
|
|
526
|
+
</aside>
|
|
527
|
+
|
|
528
|
+
<aside>
|
|
529
|
+
<h2>History</h2>
|
|
530
|
+
<ol class="history">
|
|
531
|
+
<template x-for="entry in data.history.slice(-8)" :key="entry.at + entry.action">
|
|
532
|
+
<li><span x-text="shortDate(entry.at)"></span> - <span x-text="entry.action"></span></li>
|
|
533
|
+
</template>
|
|
534
|
+
</ol>
|
|
535
|
+
</aside>
|
|
536
|
+
</div>
|
|
537
|
+
</main>
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<script id="SPEC" type="application/json">
|
|
541
|
+
{
|
|
542
|
+
"format": "htmlspec",
|
|
543
|
+
"version": "0.1.0",
|
|
544
|
+
"id": "change-id",
|
|
545
|
+
"title": "HTMLSpec Change",
|
|
546
|
+
"status": "draft",
|
|
547
|
+
"owner": "agent",
|
|
548
|
+
"createdAt": "2026-05-20T05:11:58.708Z",
|
|
549
|
+
"updatedAt": "2026-05-20T05:11:58.708Z",
|
|
550
|
+
"summaryHtml": "<p>One-paragraph summary of the change and why it matters.</p>",
|
|
551
|
+
"proposal": {
|
|
552
|
+
"whyHtml": "<p>Explain the motivation for this change. What problem does this solve? Why now?</p>",
|
|
553
|
+
"whatChanges": [
|
|
554
|
+
"Describe new capabilities, modifications, or removals. Mark breaking changes with BREAKING."
|
|
555
|
+
],
|
|
556
|
+
"capabilities": {
|
|
557
|
+
"new": [
|
|
558
|
+
{
|
|
559
|
+
"name": "capability-name",
|
|
560
|
+
"description": "Brief description of the capability this change introduces."
|
|
561
|
+
}
|
|
562
|
+
],
|
|
563
|
+
"modified": []
|
|
564
|
+
},
|
|
565
|
+
"impact": [
|
|
566
|
+
"Affected code, APIs, dependencies, or systems."
|
|
567
|
+
]
|
|
568
|
+
},
|
|
569
|
+
"spec": {
|
|
570
|
+
"overviewHtml": "<p>Create one requirements delta per capability listed in proposal.capabilities. Use SHALL/MUST and testable scenarios.</p>",
|
|
571
|
+
"operations": [
|
|
572
|
+
{
|
|
573
|
+
"type": "ADDED",
|
|
574
|
+
"requirements": [
|
|
575
|
+
{
|
|
576
|
+
"id": "REQ-001",
|
|
577
|
+
"title": "Primary behavior",
|
|
578
|
+
"bodyHtml": "<p>The system SHALL provide the user-visible behavior described by this change.</p>",
|
|
579
|
+
"scenarios": [
|
|
580
|
+
{
|
|
581
|
+
"id": "SCN-001",
|
|
582
|
+
"title": "Happy path",
|
|
583
|
+
"given": "the user is in the expected starting state",
|
|
584
|
+
"when": "the user performs the new action",
|
|
585
|
+
"then": "the system produces the expected result"
|
|
586
|
+
}
|
|
587
|
+
]
|
|
588
|
+
}
|
|
589
|
+
]
|
|
590
|
+
}
|
|
591
|
+
]
|
|
592
|
+
},
|
|
593
|
+
"design": {
|
|
594
|
+
"contextHtml": "<p>Describe background, current state, constraints, and stakeholders.</p>",
|
|
595
|
+
"goals": [
|
|
596
|
+
"State what this design achieves."
|
|
597
|
+
],
|
|
598
|
+
"nonGoals": [
|
|
599
|
+
"State what is explicitly out of scope."
|
|
600
|
+
],
|
|
601
|
+
"decisions": [
|
|
602
|
+
{
|
|
603
|
+
"id": "architecture",
|
|
604
|
+
"title": "Architecture",
|
|
605
|
+
"decisionHtml": "<p>Describe the key modules, data flow, and boundaries.</p>",
|
|
606
|
+
"rationaleHtml": "<p>Explain why this approach is preferred.</p>",
|
|
607
|
+
"alternatives": [
|
|
608
|
+
"List meaningful alternatives considered."
|
|
609
|
+
]
|
|
610
|
+
}
|
|
611
|
+
],
|
|
612
|
+
"risksTradeoffs": [
|
|
613
|
+
"Risk or trade-off -> mitigation."
|
|
614
|
+
],
|
|
615
|
+
"migrationPlan": [
|
|
616
|
+
"Deployment, rollback, or migration step if applicable."
|
|
617
|
+
],
|
|
618
|
+
"openQuestions": [
|
|
619
|
+
"List unresolved decisions before implementation starts."
|
|
620
|
+
]
|
|
621
|
+
},
|
|
622
|
+
"tasks": [
|
|
623
|
+
{
|
|
624
|
+
"id": "1",
|
|
625
|
+
"title": "Setup",
|
|
626
|
+
"items": [
|
|
627
|
+
{
|
|
628
|
+
"id": "1.1",
|
|
629
|
+
"title": "Create or update module structure",
|
|
630
|
+
"status": "todo"
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
"id": "1.2",
|
|
634
|
+
"title": "Add dependencies or configuration",
|
|
635
|
+
"status": "todo"
|
|
636
|
+
}
|
|
637
|
+
]
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
"id": "2",
|
|
641
|
+
"title": "Implementation",
|
|
642
|
+
"items": [
|
|
643
|
+
{
|
|
644
|
+
"id": "2.1",
|
|
645
|
+
"title": "Implement behavior defined by the spec operations",
|
|
646
|
+
"status": "todo"
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
"id": "2.2",
|
|
650
|
+
"title": "Add tests for each acceptance scenario",
|
|
651
|
+
"status": "todo"
|
|
652
|
+
}
|
|
653
|
+
]
|
|
654
|
+
}
|
|
655
|
+
],
|
|
656
|
+
"history": [
|
|
657
|
+
{
|
|
658
|
+
"at": "2026-05-20T05:11:58.708Z",
|
|
659
|
+
"action": "created"
|
|
660
|
+
}
|
|
661
|
+
]
|
|
662
|
+
}
|
|
663
|
+
</script>
|
|
664
|
+
|
|
665
|
+
<script>
|
|
666
|
+
function htmlspec() {
|
|
667
|
+
return {
|
|
668
|
+
data: null,
|
|
669
|
+
load() {
|
|
670
|
+
this.data = JSON.parse(document.getElementById("SPEC").textContent);
|
|
671
|
+
},
|
|
672
|
+
taskCount() {
|
|
673
|
+
return this.data.tasks.flatMap((group) => group.items).length;
|
|
674
|
+
},
|
|
675
|
+
doneCount() {
|
|
676
|
+
return this.data.tasks.flatMap((group) => group.items).filter((task) => task.status === "done").length;
|
|
677
|
+
},
|
|
678
|
+
completion() {
|
|
679
|
+
const total = this.taskCount();
|
|
680
|
+
return total === 0 ? 0 : Math.round((this.doneCount() / total) * 100);
|
|
681
|
+
},
|
|
682
|
+
statusClass(status) {
|
|
683
|
+
return `status-${status}`;
|
|
684
|
+
},
|
|
685
|
+
shortDate(value) {
|
|
686
|
+
return String(value).replace("T", " ").replace(/\.\d{3}Z$/, "Z");
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
</script>
|
|
691
|
+
</body>
|
|
692
|
+
</html>
|