@leonarto/spec-embryo 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.
Files changed (62) hide show
  1. package/README.md +156 -0
  2. package/package.json +48 -0
  3. package/src/backends/base.ts +18 -0
  4. package/src/backends/deterministic.ts +105 -0
  5. package/src/backends/index.ts +26 -0
  6. package/src/backends/prompt.ts +169 -0
  7. package/src/backends/subprocess.ts +198 -0
  8. package/src/cli.ts +111 -0
  9. package/src/commands/agents.ts +16 -0
  10. package/src/commands/current.ts +95 -0
  11. package/src/commands/doctor.ts +12 -0
  12. package/src/commands/handoff.ts +64 -0
  13. package/src/commands/init.ts +101 -0
  14. package/src/commands/reshape.ts +20 -0
  15. package/src/commands/resume.ts +19 -0
  16. package/src/commands/spec.ts +108 -0
  17. package/src/commands/status.ts +98 -0
  18. package/src/commands/task.ts +190 -0
  19. package/src/commands/ui.ts +35 -0
  20. package/src/domain.ts +357 -0
  21. package/src/engine.ts +290 -0
  22. package/src/frontmatter.ts +83 -0
  23. package/src/index.ts +75 -0
  24. package/src/paths.ts +32 -0
  25. package/src/repository.ts +807 -0
  26. package/src/services/adoption.ts +169 -0
  27. package/src/services/agents.ts +191 -0
  28. package/src/services/dashboard.ts +776 -0
  29. package/src/services/details.ts +453 -0
  30. package/src/services/doctor.ts +452 -0
  31. package/src/services/layout.ts +420 -0
  32. package/src/services/spec-answer-evaluation.ts +103 -0
  33. package/src/services/spec-import.ts +217 -0
  34. package/src/services/spec-questions.ts +343 -0
  35. package/src/services/ui.ts +34 -0
  36. package/src/storage.ts +57 -0
  37. package/src/templates.ts +270 -0
  38. package/tsconfig.json +17 -0
  39. package/web/package.json +24 -0
  40. package/web/src/app.css +83 -0
  41. package/web/src/app.d.ts +6 -0
  42. package/web/src/app.html +11 -0
  43. package/web/src/lib/components/AnalysisFilters.svelte +293 -0
  44. package/web/src/lib/components/DocumentBody.svelte +100 -0
  45. package/web/src/lib/components/MultiSelectDropdown.svelte +280 -0
  46. package/web/src/lib/components/SelectDropdown.svelte +265 -0
  47. package/web/src/lib/server/project-root.ts +34 -0
  48. package/web/src/lib/task-board.ts +20 -0
  49. package/web/src/routes/+layout.server.ts +57 -0
  50. package/web/src/routes/+layout.svelte +421 -0
  51. package/web/src/routes/+layout.ts +1 -0
  52. package/web/src/routes/+page.svelte +530 -0
  53. package/web/src/routes/specs/+page.svelte +416 -0
  54. package/web/src/routes/specs/[specId]/+page.server.ts +81 -0
  55. package/web/src/routes/specs/[specId]/+page.svelte +675 -0
  56. package/web/src/routes/tasks/+page.svelte +341 -0
  57. package/web/src/routes/tasks/[taskId]/+page.server.ts +12 -0
  58. package/web/src/routes/tasks/[taskId]/+page.svelte +431 -0
  59. package/web/src/routes/timeline/+page.svelte +1093 -0
  60. package/web/svelte.config.js +10 -0
  61. package/web/tsconfig.json +9 -0
  62. package/web/vite.config.ts +11 -0
@@ -0,0 +1,416 @@
1
+ <script lang="ts">
2
+ import { page } from "$app/state";
3
+ import { CircleCheckBig, Gauge, NotebookText, Orbit, Pause, RotateCcw, SlidersHorizontal, Sparkles, TimerReset } from "@lucide/svelte";
4
+ import type { DashboardData } from "../../../../src/services/dashboard.ts";
5
+ import AnalysisFilters from "../../lib/components/AnalysisFilters.svelte";
6
+ import MultiSelectDropdown from "../../lib/components/MultiSelectDropdown.svelte";
7
+
8
+ let { data } = $props();
9
+ const dashboard = $derived(data.dashboard as DashboardData);
10
+ const selectedSpecIds = $derived(new Set(dashboard.analysis.selectedSpecIds));
11
+ const currentPath = $derived(page.url.pathname);
12
+ const currentSearch = $derived(page.url.searchParams);
13
+
14
+ const statusIcons = {
15
+ proposed: Sparkles,
16
+ active: Orbit,
17
+ paused: Pause,
18
+ done: CircleCheckBig,
19
+ archived: NotebookText,
20
+ } as const;
21
+
22
+ const specStatusOrder = {
23
+ active: 0,
24
+ proposed: 1,
25
+ paused: 2,
26
+ done: 3,
27
+ archived: 4,
28
+ } as const;
29
+
30
+ const statusOptions = [
31
+ { value: "active", label: "Active", description: "Live product intent still being extended or executed." },
32
+ { value: "proposed", label: "Proposed", description: "Planned work that is drafted but not yet activated." },
33
+ { value: "paused", label: "Paused", description: "Known intent that is intentionally on hold." },
34
+ { value: "done", label: "Done", description: "Completed slices that remain as durable project history." },
35
+ { value: "archived", label: "Archived", description: "Historical specs that are no longer part of the active product arc." },
36
+ ] as const;
37
+
38
+ const selectedStatuses = $derived(
39
+ Array.from(new Set(currentSearch.getAll("spec_status").map((status) => status.trim()).filter((status) => status in specStatusOrder))),
40
+ );
41
+ const selectedStatusSet = $derived(new Set(selectedStatuses));
42
+ const visibleSpecs = $derived(
43
+ dashboard.specs
44
+ .filter((spec) => (dashboard.analysis.selectedSpecIds.length === 0 ? true : selectedSpecIds.has(spec.id)))
45
+ .filter((spec) => (selectedStatuses.length === 0 ? true : selectedStatusSet.has(spec.status)))
46
+ .sort(
47
+ (left, right) =>
48
+ specStatusOrder[left.status as keyof typeof specStatusOrder] -
49
+ specStatusOrder[right.status as keyof typeof specStatusOrder] ||
50
+ left.id.localeCompare(right.id),
51
+ ),
52
+ );
53
+ </script>
54
+
55
+ <svelte:head>
56
+ <title>{dashboard.project.name} Specs</title>
57
+ </svelte:head>
58
+
59
+ <section class="view-header">
60
+ <div class="header-copy">
61
+ <p class="eyebrow">Source-aligned intent</p>
62
+ <h3>Specs View</h3>
63
+ </div>
64
+ <p class="intro">
65
+ Specs stay stable and task-linked here. This view is for seeing intent, current execution weight, and completion shape without losing the narrative layer.
66
+ </p>
67
+ </section>
68
+
69
+ <AnalysisFilters
70
+ {dashboard}
71
+ title="Spec slice filters"
72
+ detail="Use one or more specs, lifecycle windows, and spec states to narrow the intent surface. Specs stay ordered from active work through historical work."
73
+ resultCount={visibleSpecs.length}
74
+ resultLabel="specs in the current view"
75
+ hasActiveFilters={dashboard.analysis.hasActiveFilters || selectedStatuses.length > 0}
76
+ inactiveDetail="Without filters, the full spec inventory stays visible in status-first order."
77
+ activeDetail="Analytical spec and state filters are active."
78
+ clearHref={currentPath}
79
+ >
80
+ {#snippet extraField()}
81
+ <fieldset class="field-card">
82
+ <legend>
83
+ <SlidersHorizontal size={14} strokeWidth={1.9} />
84
+ Spec states
85
+ </legend>
86
+ <MultiSelectDropdown
87
+ name="spec_status"
88
+ options={statusOptions}
89
+ selectedValues={selectedStatuses}
90
+ buttonLabel="Choose states"
91
+ emptyLabel="All states"
92
+ />
93
+ </fieldset>
94
+ {/snippet}
95
+ </AnalysisFilters>
96
+
97
+ <section class="spec-grid-shell">
98
+ {#if visibleSpecs.length === 0}
99
+ <div class="analysis-empty">
100
+ <TimerReset size={16} strokeWidth={1.9} />
101
+ <span>No specs matched the current state and analytical filters.</span>
102
+ </div>
103
+ {:else}
104
+ <div class="spec-grid">
105
+ {#each visibleSpecs as spec}
106
+ {@const StatusIcon = statusIcons[spec.status as keyof typeof statusIcons] ?? NotebookText}
107
+ <article class:selected={selectedSpecIds.has(spec.id)} class="spec-card">
108
+ <div class="card-head">
109
+ <div class="card-copy">
110
+ <div class="card-topline">
111
+ <p class="spec-id">{spec.id}</p>
112
+ <div class="card-meta">
113
+ <span class="mini-pill neutral">{spec.linkedTaskCount} tasks</span>
114
+ <span class={`status ${spec.status}`}>
115
+ <StatusIcon size={13} strokeWidth={1.9} />
116
+ {spec.status}
117
+ </span>
118
+ </div>
119
+ </div>
120
+ <h4><a class="title-link" href={`/specs/${spec.id}`} title={spec.title}>{spec.title}</a></h4>
121
+ </div>
122
+ </div>
123
+
124
+ <p>{spec.summary}</p>
125
+
126
+ <div class="progress-shell">
127
+ <div class="progress-copy">
128
+ <div class="mini-head">
129
+ <Gauge size={14} strokeWidth={1.9} />
130
+ <span>Completion</span>
131
+ </div>
132
+ <strong>{Math.round(spec.progress.completionRatio * 100)}%</strong>
133
+ </div>
134
+ <div class="progress-strip">
135
+ <div class="progress-bar" style={`width: ${Math.max(spec.progress.completionRatio * 100, 6)}%`}></div>
136
+ </div>
137
+ </div>
138
+
139
+ <div class="progress-grid">
140
+ <span>done {spec.progress.done}</span>
141
+ <span>in progress {spec.progress.inProgress}</span>
142
+ <span>blocked {spec.progress.blocked}</span>
143
+ <span>review {spec.progress.review}</span>
144
+ <span>todo {spec.progress.todo}</span>
145
+ </div>
146
+
147
+ <div class="chip-row">
148
+ {#each spec.taskIds as taskId}
149
+ <a class="chip" href={`/tasks/${taskId}`}>{taskId}</a>
150
+ {/each}
151
+ </div>
152
+ </article>
153
+ {/each}
154
+ </div>
155
+ {/if}
156
+ </section>
157
+
158
+ <style>
159
+ .view-header {
160
+ display: flex;
161
+ justify-content: space-between;
162
+ gap: 1rem;
163
+ align-items: end;
164
+ margin-bottom: 1rem;
165
+ }
166
+
167
+ .header-copy {
168
+ display: grid;
169
+ gap: 0.18rem;
170
+ }
171
+
172
+ .eyebrow {
173
+ margin: 0 0 0.35rem;
174
+ text-transform: uppercase;
175
+ letter-spacing: 0.14em;
176
+ font-size: 0.68rem;
177
+ color: var(--muted);
178
+ }
179
+
180
+ h3,
181
+ h4 {
182
+ margin: 0;
183
+ font-family: var(--display-font);
184
+ }
185
+
186
+ h3 {
187
+ font-size: clamp(1.55rem, 1.8vw, 2rem);
188
+ }
189
+
190
+ h4 {
191
+ font-size: 1.24rem;
192
+ }
193
+
194
+ .intro,
195
+ .spec-card p,
196
+ .progress-grid {
197
+ color: var(--muted);
198
+ line-height: 1.45;
199
+ }
200
+
201
+ .spec-grid-shell {
202
+ display: grid;
203
+ gap: 0.9rem;
204
+ margin-bottom: 1rem;
205
+ padding: 1rem;
206
+ border-radius: var(--radius-xl);
207
+ border: 1px solid var(--line);
208
+ background: var(--panel);
209
+ box-shadow: var(--shadow);
210
+ }
211
+
212
+ .spec-grid {
213
+ display: grid;
214
+ grid-template-columns: repeat(2, minmax(0, 1fr));
215
+ gap: 0.9rem;
216
+ }
217
+
218
+ .spec-card {
219
+ display: grid;
220
+ gap: 0.9rem;
221
+ padding: 1rem;
222
+ border-radius: var(--radius-xl);
223
+ border: 1px solid var(--line);
224
+ background: var(--panel);
225
+ box-shadow: var(--shadow);
226
+ }
227
+
228
+ .spec-card.selected {
229
+ border-color: rgba(15, 141, 96, 0.22);
230
+ background: linear-gradient(180deg, rgba(255, 253, 249, 0.96), rgba(239, 248, 243, 0.9));
231
+ }
232
+
233
+ .card-head,
234
+ .mini-head,
235
+ .progress-copy,
236
+ .card-topline,
237
+ .analysis-topline {
238
+ display: flex;
239
+ align-items: center;
240
+ }
241
+
242
+ .card-head,
243
+ .progress-copy,
244
+ .card-topline,
245
+ .analysis-topline {
246
+ justify-content: space-between;
247
+ gap: 1rem;
248
+ }
249
+
250
+ .card-copy,
251
+ .analysis-copy {
252
+ display: grid;
253
+ gap: 0.2rem;
254
+ min-width: 0;
255
+ }
256
+
257
+ .mini-head {
258
+ gap: 0.4rem;
259
+ color: var(--muted-soft);
260
+ }
261
+
262
+ .spec-id,
263
+ .status {
264
+ margin: 0;
265
+ font-size: 0.76rem;
266
+ text-transform: uppercase;
267
+ letter-spacing: 0.12em;
268
+ color: var(--muted);
269
+ }
270
+
271
+ .card-meta,
272
+ .analysis-meta {
273
+ display: flex;
274
+ align-items: center;
275
+ justify-content: flex-end;
276
+ gap: 0.38rem;
277
+ flex-wrap: wrap;
278
+ }
279
+
280
+ .status,
281
+ .mini-pill,
282
+ .chip,
283
+ .status-pill {
284
+ display: inline-flex;
285
+ align-items: center;
286
+ gap: 0.32rem;
287
+ border-radius: 999px;
288
+ padding: 0.18rem 0.48rem;
289
+ border: 1px solid var(--line);
290
+ background: rgba(255, 255, 255, 0.88);
291
+ box-shadow:
292
+ 0 0 0 1px rgba(255, 255, 255, 0.34) inset,
293
+ var(--pill-glow, var(--shadow-soft));
294
+ font-size: 0.71rem;
295
+ line-height: 1.1;
296
+ }
297
+
298
+ .progress-shell {
299
+ display: grid;
300
+ gap: 0.55rem;
301
+ }
302
+
303
+ .progress-copy strong {
304
+ font-family: var(--display-font);
305
+ font-size: 1.15rem;
306
+ line-height: 1;
307
+ }
308
+
309
+ .progress-strip {
310
+ height: 0.72rem;
311
+ border-radius: 999px;
312
+ overflow: hidden;
313
+ background: rgba(24, 22, 18, 0.08);
314
+ }
315
+
316
+ .progress-bar {
317
+ height: 100%;
318
+ border-radius: 999px;
319
+ background: linear-gradient(90deg, rgba(15, 141, 96, 0.78), rgba(15, 141, 96, 0.36));
320
+ }
321
+
322
+ .progress-grid,
323
+ .chip-row {
324
+ display: flex;
325
+ flex-wrap: wrap;
326
+ gap: 0.5rem;
327
+ }
328
+
329
+ .title-link {
330
+ text-decoration: none;
331
+ }
332
+
333
+ .mini-pill.neutral {
334
+ border-color: rgba(24, 22, 18, 0.1);
335
+ background: rgba(255, 255, 255, 0.82);
336
+ --pill-glow: 0 10px 20px rgba(24, 22, 18, 0.05);
337
+ }
338
+
339
+ .status.active {
340
+ color: #0d6e4b;
341
+ border-color: rgba(15, 141, 96, 0.2);
342
+ background: rgba(15, 141, 96, 0.14);
343
+ --pill-glow: 0 0 16px rgba(15, 141, 96, 0.14);
344
+ }
345
+
346
+ .status.proposed {
347
+ color: #3456a0;
348
+ border-color: rgba(70, 107, 202, 0.2);
349
+ background: rgba(70, 107, 202, 0.12);
350
+ --pill-glow: 0 0 16px rgba(70, 107, 202, 0.12);
351
+ }
352
+
353
+ .status.paused {
354
+ color: #755733;
355
+ border-color: rgba(125, 96, 58, 0.22);
356
+ background: rgba(125, 96, 58, 0.14);
357
+ --pill-glow: 0 0 16px rgba(125, 96, 58, 0.12);
358
+ }
359
+
360
+ .status.done {
361
+ color: #0c6d4a;
362
+ border-color: rgba(17, 122, 84, 0.18);
363
+ background: rgba(17, 122, 84, 0.12);
364
+ --pill-glow: 0 0 16px rgba(17, 122, 84, 0.1);
365
+ }
366
+
367
+ .status.archived {
368
+ color: #6e5434;
369
+ border-color: rgba(117, 92, 58, 0.18);
370
+ background: rgba(117, 92, 58, 0.14);
371
+ --pill-glow: 0 0 16px rgba(117, 92, 58, 0.1);
372
+ }
373
+
374
+ .status-pill.review {
375
+ color: #8f5421;
376
+ border-color: rgba(143, 84, 33, 0.22);
377
+ background: rgba(143, 84, 33, 0.14);
378
+ --pill-glow: 0 0 16px rgba(143, 84, 33, 0.14);
379
+ }
380
+
381
+ .status-pill.blocked {
382
+ color: #8d2d2d;
383
+ border-color: rgba(151, 45, 45, 0.22);
384
+ background: rgba(151, 45, 45, 0.12);
385
+ --pill-glow: 0 0 16px rgba(151, 45, 45, 0.14);
386
+ }
387
+
388
+ .status-pill.todo {
389
+ color: #5d5549;
390
+ border-color: rgba(84, 78, 67, 0.14);
391
+ background: rgba(84, 78, 67, 0.1);
392
+ --pill-glow: 0 0 14px rgba(84, 78, 67, 0.08);
393
+ }
394
+
395
+ .analysis-empty {
396
+ display: inline-flex;
397
+ align-items: center;
398
+ gap: 0.5rem;
399
+ color: var(--muted);
400
+ padding: 0.9rem 1rem;
401
+ border-radius: var(--radius-lg);
402
+ border: 1px dashed var(--line-strong);
403
+ background: rgba(255, 255, 255, 0.6);
404
+ }
405
+
406
+ @media (max-width: 900px) {
407
+ .view-header {
408
+ flex-direction: column;
409
+ align-items: start;
410
+ }
411
+
412
+ .spec-grid {
413
+ grid-template-columns: 1fr;
414
+ }
415
+ }
416
+ </style>
@@ -0,0 +1,81 @@
1
+ import { fail } from "@sveltejs/kit";
2
+ import type { Actions, PageServerLoad } from "./$types";
3
+ import { answerSpecOpenQuestions, loadProjectContext } from "../../../../../src/repository.ts";
4
+ import { buildSpecAnswerEvaluation } from "../../../../../src/services/spec-answer-evaluation.ts";
5
+ import { buildSpecDetailData } from "../../../../../src/services/details.ts";
6
+ import { resolveProjectRoot } from "../../../lib/server/project-root.ts";
7
+
8
+ export const load: PageServerLoad = async ({ params }) => {
9
+ const rootDir = resolveProjectRoot();
10
+ const detail = await buildSpecDetailData(rootDir, params.specId);
11
+
12
+ return {
13
+ detail,
14
+ };
15
+ };
16
+
17
+ export const actions: Actions = {
18
+ answerOpenQuestions: async ({ params, request }) => {
19
+ const rootDir = resolveProjectRoot();
20
+ const formData = await request.formData();
21
+ const specId = params.specId;
22
+ const questions = formData.getAll("question");
23
+ const answers = formData.getAll("answer");
24
+ const answeredPairs = questions.flatMap((value, index) => {
25
+ const question = typeof value === "string" ? value.trim() : "";
26
+ const answer = typeof answers[index] === "string" ? answers[index].trim() : "";
27
+
28
+ if (!question || !answer) {
29
+ return [];
30
+ }
31
+
32
+ return [{ question, answer }];
33
+ });
34
+
35
+ if (answeredPairs.length === 0) {
36
+ return fail(400, {
37
+ action: "answerOpenQuestions",
38
+ error: "Add at least one answer before saving.",
39
+ });
40
+ }
41
+
42
+ try {
43
+ await answerSpecOpenQuestions(rootDir, specId, answeredPairs);
44
+ const detail = await buildSpecDetailData(rootDir, specId);
45
+ if (!detail.found) {
46
+ return fail(404, {
47
+ action: "answerOpenQuestions",
48
+ error: `Spec ${specId} was not found after the update.`,
49
+ });
50
+ }
51
+
52
+ const context = await loadProjectContext(rootDir);
53
+ const spec = context.specs.find((entry) => entry.id === specId);
54
+ if (!spec) {
55
+ return fail(404, {
56
+ action: "answerOpenQuestions",
57
+ error: `Spec ${specId} was not found after the update.`,
58
+ });
59
+ }
60
+
61
+ return {
62
+ action: "answerOpenQuestions",
63
+ success: true,
64
+ savedCount: answeredPairs.length,
65
+ answeredQuestions: answeredPairs.map((entry) => entry.question),
66
+ evaluation: buildSpecAnswerEvaluation({
67
+ spec,
68
+ linkedTasks: detail.linkedTasks,
69
+ answeredQuestions: answeredPairs.map((entry) => entry.question),
70
+ remainingOpenQuestions: detail.openQuestions,
71
+ resolvedDecisions: detail.resolvedDecisions,
72
+ }),
73
+ };
74
+ } catch (error) {
75
+ return fail(400, {
76
+ action: "answerOpenQuestions",
77
+ error: error instanceof Error ? error.message : "Failed to answer open questions.",
78
+ });
79
+ }
80
+ },
81
+ };