@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,453 @@
1
+ import { buildTaskIndex, summarizeTaskNode } from "../engine.ts";
2
+ import type {
3
+ CurrentStateDocument,
4
+ HandoffDocument,
5
+ ProjectContext,
6
+ SpecDocument,
7
+ TaskDocument,
8
+ } from "../domain.ts";
9
+ import { loadProjectContext } from "../repository.ts";
10
+ import { deriveEffectiveStatus, type DashboardTaskCard } from "./dashboard.ts";
11
+ import { parseSpecQuestionSections } from "./spec-questions.ts";
12
+
13
+ export interface DocumentBodyBlock {
14
+ kind: "paragraph" | "list";
15
+ lines: string[];
16
+ }
17
+
18
+ export interface DocumentBodySection {
19
+ heading: string;
20
+ depth: number;
21
+ blocks: DocumentBodyBlock[];
22
+ }
23
+
24
+ export interface DetailLinkedSpec {
25
+ id: string;
26
+ title: string;
27
+ status: string;
28
+ }
29
+
30
+ export interface DetailLinkedTask extends DashboardTaskCard {
31
+ statusLabel: string;
32
+ }
33
+
34
+ export interface DetailCurrentStateRelevance {
35
+ isActiveSpec: boolean;
36
+ isActiveTask: boolean;
37
+ isBlockedTask: boolean;
38
+ focus: string;
39
+ }
40
+
41
+ export interface DetailRelatedHandoff {
42
+ id: string;
43
+ title: string;
44
+ summary: string;
45
+ createdAt: string;
46
+ }
47
+
48
+ export interface SpecOpenQuestionDetail {
49
+ id: string;
50
+ question: string;
51
+ }
52
+
53
+ export interface ResolvedDecisionDetail {
54
+ id: string;
55
+ question?: string;
56
+ answer: string;
57
+ }
58
+
59
+ export interface SpecDetailData {
60
+ kind: "spec";
61
+ found: true;
62
+ spec: {
63
+ id: string;
64
+ title: string;
65
+ summary: string;
66
+ status: string;
67
+ tags: string[];
68
+ owner?: string;
69
+ updatedAt?: string;
70
+ };
71
+ progress: {
72
+ todo: number;
73
+ inProgress: number;
74
+ blocked: number;
75
+ review: number;
76
+ done: number;
77
+ cancelled: number;
78
+ completionRatio: number;
79
+ };
80
+ linkedTasks: DetailLinkedTask[];
81
+ linkedTaskGroups: Array<{
82
+ id: string;
83
+ title: string;
84
+ tasks: DetailLinkedTask[];
85
+ }>;
86
+ currentState: DetailCurrentStateRelevance;
87
+ relatedHandoffs: DetailRelatedHandoff[];
88
+ openQuestions: SpecOpenQuestionDetail[];
89
+ resolvedDecisions: ResolvedDecisionDetail[];
90
+ bodySections: DocumentBodySection[];
91
+ }
92
+
93
+ export interface TaskDetailData {
94
+ kind: "task";
95
+ found: true;
96
+ task: {
97
+ id: string;
98
+ title: string;
99
+ summary: string;
100
+ status: string;
101
+ priority: number;
102
+ ownerRole?: string;
103
+ tags: string[];
104
+ updatedAt?: string;
105
+ createdAt?: string;
106
+ completedAt?: string;
107
+ cancelledAt?: string;
108
+ archivedAt?: string;
109
+ archiveReason?: string;
110
+ };
111
+ linkedSpecs: DetailLinkedSpec[];
112
+ dependencies: DetailLinkedTask[];
113
+ unmetDependencyIds: string[];
114
+ blockingTasks: DetailLinkedTask[];
115
+ currentState: DetailCurrentStateRelevance;
116
+ relatedHandoffs: DetailRelatedHandoff[];
117
+ bodySections: DocumentBodySection[];
118
+ }
119
+
120
+ export interface DetailNotFoundData {
121
+ found: false;
122
+ kind: "spec" | "task";
123
+ id: string;
124
+ availableIds: string[];
125
+ }
126
+
127
+ export type SpecDetailResult = SpecDetailData | DetailNotFoundData;
128
+ export type TaskDetailResult = TaskDetailData | DetailNotFoundData;
129
+
130
+ function deriveEffort(priority: number, downstreamCount: number): number {
131
+ const base = priority <= 1 ? 5 : priority === 2 ? 3 : 2;
132
+ return Math.min(8, base + Math.min(2, downstreamCount));
133
+ }
134
+
135
+ function toDetailTaskCard(task: TaskDocument, index: ReturnType<typeof buildTaskIndex>): DetailLinkedTask {
136
+ const summary = summarizeTaskNode(task, index);
137
+ const effectiveStatus = deriveEffectiveStatus(task.status, summary.unmetDependencies);
138
+
139
+ return {
140
+ id: task.id,
141
+ title: task.title,
142
+ summary: task.summary,
143
+ rawStatus: task.status,
144
+ effectiveStatus,
145
+ priority: task.priority,
146
+ specIds: task.specIds,
147
+ dependsOn: task.dependsOn,
148
+ unmetDependencies: summary.unmetDependencies,
149
+ downstreamCount: summary.downstreamCount,
150
+ effort: deriveEffort(task.priority, summary.downstreamCount),
151
+ completedAt: task.completedAt,
152
+ cancelledAt: task.cancelledAt,
153
+ archivedAt: task.archivedAt,
154
+ archiveReason: task.archiveReason,
155
+ statusLabel: task.status.replaceAll("_", " "),
156
+ };
157
+ }
158
+
159
+ function buildSpecProgress(taskCards: DetailLinkedTask[]) {
160
+ const counts = {
161
+ todo: 0,
162
+ inProgress: 0,
163
+ blocked: 0,
164
+ review: 0,
165
+ done: 0,
166
+ cancelled: 0,
167
+ };
168
+
169
+ for (const task of taskCards) {
170
+ if (task.effectiveStatus === "in_progress") {
171
+ counts.inProgress += 1;
172
+ continue;
173
+ }
174
+
175
+ if (task.effectiveStatus === "blocked") {
176
+ counts.blocked += 1;
177
+ continue;
178
+ }
179
+
180
+ if (task.effectiveStatus === "review") {
181
+ counts.review += 1;
182
+ continue;
183
+ }
184
+
185
+ counts[task.effectiveStatus] += 1;
186
+ }
187
+
188
+ const total = taskCards.length;
189
+ return {
190
+ todo: counts.todo,
191
+ inProgress: counts.inProgress,
192
+ blocked: counts.blocked,
193
+ review: counts.review,
194
+ done: counts.done,
195
+ cancelled: counts.cancelled,
196
+ completionRatio: total === 0 ? 0 : counts.done / total,
197
+ };
198
+ }
199
+
200
+ function buildLinkedTaskGroups(tasks: DetailLinkedTask[]): Array<{ id: string; title: string; tasks: DetailLinkedTask[] }> {
201
+ const groups = [
202
+ { id: "in_progress", title: "In Progress" },
203
+ { id: "blocked", title: "Blocked" },
204
+ { id: "review", title: "Review" },
205
+ { id: "todo", title: "Todo" },
206
+ { id: "done", title: "Done" },
207
+ { id: "cancelled", title: "Cancelled" },
208
+ ];
209
+
210
+ return groups
211
+ .map((group) => ({
212
+ ...group,
213
+ tasks: tasks.filter((task) => task.effectiveStatus === group.id),
214
+ }))
215
+ .filter((group) => group.tasks.length > 0);
216
+ }
217
+
218
+ function parseDocumentBody(body: string): DocumentBodySection[] {
219
+ const normalized = body.trim();
220
+ if (!normalized) {
221
+ return [];
222
+ }
223
+
224
+ const sections: DocumentBodySection[] = [];
225
+ let currentSection: DocumentBodySection | undefined;
226
+ let paragraph: string[] = [];
227
+ let list: string[] = [];
228
+
229
+ function ensureSection(): DocumentBodySection {
230
+ if (!currentSection) {
231
+ currentSection = {
232
+ heading: "Overview",
233
+ depth: 2,
234
+ blocks: [],
235
+ };
236
+ sections.push(currentSection);
237
+ }
238
+
239
+ return currentSection;
240
+ }
241
+
242
+ function flushParagraph() {
243
+ if (paragraph.length === 0) {
244
+ return;
245
+ }
246
+
247
+ ensureSection().blocks.push({
248
+ kind: "paragraph",
249
+ lines: [paragraph.join(" ").trim()],
250
+ });
251
+ paragraph = [];
252
+ }
253
+
254
+ function flushList() {
255
+ if (list.length === 0) {
256
+ return;
257
+ }
258
+
259
+ ensureSection().blocks.push({
260
+ kind: "list",
261
+ lines: [...list],
262
+ });
263
+ list = [];
264
+ }
265
+
266
+ for (const line of normalized.split("\n")) {
267
+ const trimmed = line.trim();
268
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
269
+
270
+ if (headingMatch) {
271
+ flushParagraph();
272
+ flushList();
273
+ currentSection = {
274
+ heading: headingMatch[2]!.trim(),
275
+ depth: headingMatch[1]!.length,
276
+ blocks: [],
277
+ };
278
+ sections.push(currentSection);
279
+ continue;
280
+ }
281
+
282
+ if (trimmed.length === 0) {
283
+ flushParagraph();
284
+ flushList();
285
+ continue;
286
+ }
287
+
288
+ if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
289
+ flushParagraph();
290
+ list.push(trimmed.slice(2).trim());
291
+ continue;
292
+ }
293
+
294
+ flushList();
295
+ paragraph.push(trimmed);
296
+ }
297
+
298
+ flushParagraph();
299
+ flushList();
300
+ return sections;
301
+ }
302
+
303
+ function buildCurrentStateRelevance(currentState: CurrentStateDocument, input: { specId?: string; taskId?: string }): DetailCurrentStateRelevance {
304
+ return {
305
+ isActiveSpec: input.specId ? currentState.activeSpecIds.includes(input.specId) : false,
306
+ isActiveTask: input.taskId ? currentState.activeTaskIds.includes(input.taskId) : false,
307
+ isBlockedTask: input.taskId ? currentState.blockedTaskIds.includes(input.taskId) : false,
308
+ focus: currentState.focus,
309
+ };
310
+ }
311
+
312
+ function sortHandoffsDescending(handoffs: HandoffDocument[]): DetailRelatedHandoff[] {
313
+ return handoffs
314
+ .slice()
315
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))
316
+ .slice(0, 3)
317
+ .map((handoff) => ({
318
+ id: handoff.id,
319
+ title: handoff.title,
320
+ summary: handoff.summary,
321
+ createdAt: handoff.createdAt,
322
+ }));
323
+ }
324
+
325
+ function buildRelatedHandoffsForSpec(context: ProjectContext, spec: SpecDocument): DetailRelatedHandoff[] {
326
+ return sortHandoffsDescending(
327
+ context.handoffs.filter(
328
+ (handoff) =>
329
+ handoff.relatedSpecIds.includes(spec.id) ||
330
+ handoff.activeTaskIds.some((taskId) => spec.taskIds.includes(taskId)),
331
+ ),
332
+ );
333
+ }
334
+
335
+ function buildRelatedHandoffsForTask(context: ProjectContext, task: TaskDocument): DetailRelatedHandoff[] {
336
+ return sortHandoffsDescending(
337
+ context.handoffs.filter(
338
+ (handoff) => handoff.activeTaskIds.includes(task.id) || handoff.relatedSpecIds.some((specId) => task.specIds.includes(specId)),
339
+ ),
340
+ );
341
+ }
342
+
343
+ function buildLinkedSpecs(context: ProjectContext, task: TaskDocument): DetailLinkedSpec[] {
344
+ return task.specIds
345
+ .map((specId) => context.specs.find((spec) => spec.id === specId))
346
+ .filter((spec): spec is SpecDocument => Boolean(spec))
347
+ .map((spec) => ({
348
+ id: spec.id,
349
+ title: spec.title,
350
+ status: spec.status,
351
+ }));
352
+ }
353
+
354
+ export async function buildSpecDetailData(rootDir: string, specId: string): Promise<SpecDetailResult> {
355
+ const context = await loadProjectContext(rootDir);
356
+ const spec = context.specs.find((entry) => entry.id === specId);
357
+
358
+ if (!spec) {
359
+ return {
360
+ found: false,
361
+ kind: "spec",
362
+ id: specId,
363
+ availableIds: context.specs.map((entry) => entry.id).sort(),
364
+ };
365
+ }
366
+
367
+ const taskIndex = buildTaskIndex(context.tasks);
368
+ const linkedTasks = spec.taskIds
369
+ .map((taskId) => taskIndex.get(taskId))
370
+ .filter((task): task is TaskDocument => Boolean(task))
371
+ .map((task) => toDetailTaskCard(task, taskIndex))
372
+ .sort((left, right) => left.priority - right.priority || left.id.localeCompare(right.id));
373
+ const parsedQuestionSections = parseSpecQuestionSections(spec.body);
374
+ const bodySections = parseDocumentBody(spec.body).filter((section) => {
375
+ const heading = section.heading.trim().toLowerCase();
376
+ return heading !== "open questions" && heading !== "resolved decisions";
377
+ });
378
+
379
+ return {
380
+ found: true,
381
+ kind: "spec",
382
+ spec: {
383
+ id: spec.id,
384
+ title: spec.title,
385
+ summary: spec.summary,
386
+ status: spec.status,
387
+ tags: spec.tags,
388
+ owner: spec.owner,
389
+ updatedAt: spec.updatedAt,
390
+ },
391
+ progress: buildSpecProgress(linkedTasks),
392
+ linkedTasks,
393
+ linkedTaskGroups: buildLinkedTaskGroups(linkedTasks),
394
+ currentState: buildCurrentStateRelevance(context.currentState, { specId: spec.id }),
395
+ relatedHandoffs: buildRelatedHandoffsForSpec(context, spec),
396
+ openQuestions: parsedQuestionSections.openQuestions,
397
+ resolvedDecisions: parsedQuestionSections.resolvedDecisions,
398
+ bodySections,
399
+ };
400
+ }
401
+
402
+ export async function buildTaskDetailData(rootDir: string, taskId: string): Promise<TaskDetailResult> {
403
+ const context = await loadProjectContext(rootDir);
404
+ const task = context.tasks.find((entry) => entry.id === taskId);
405
+
406
+ if (!task) {
407
+ return {
408
+ found: false,
409
+ kind: "task",
410
+ id: taskId,
411
+ availableIds: context.tasks.map((entry) => entry.id).sort(),
412
+ };
413
+ }
414
+
415
+ const taskIndex = buildTaskIndex(context.tasks);
416
+ const dependencyTasks = task.dependsOn
417
+ .map((dependencyId) => taskIndex.get(dependencyId))
418
+ .filter((dependency): dependency is TaskDocument => Boolean(dependency))
419
+ .map((dependency) => toDetailTaskCard(dependency, taskIndex))
420
+ .sort((left, right) => left.priority - right.priority || left.id.localeCompare(right.id));
421
+ const summary = summarizeTaskNode(task, taskIndex);
422
+ const blockingTasks = context.tasks
423
+ .filter((entry) => entry.dependsOn.includes(task.id))
424
+ .map((entry) => toDetailTaskCard(entry, taskIndex))
425
+ .sort((left, right) => left.priority - right.priority || left.id.localeCompare(right.id));
426
+
427
+ return {
428
+ found: true,
429
+ kind: "task",
430
+ task: {
431
+ id: task.id,
432
+ title: task.title,
433
+ summary: task.summary,
434
+ status: task.status,
435
+ priority: task.priority,
436
+ ownerRole: task.ownerRole,
437
+ tags: task.tags,
438
+ updatedAt: task.updatedAt,
439
+ createdAt: task.createdAt,
440
+ completedAt: task.completedAt,
441
+ cancelledAt: task.cancelledAt,
442
+ archivedAt: task.archivedAt,
443
+ archiveReason: task.archiveReason,
444
+ },
445
+ linkedSpecs: buildLinkedSpecs(context, task),
446
+ dependencies: dependencyTasks,
447
+ unmetDependencyIds: summary.unmetDependencies,
448
+ blockingTasks,
449
+ currentState: buildCurrentStateRelevance(context.currentState, { taskId: task.id }),
450
+ relatedHandoffs: buildRelatedHandoffsForTask(context, task),
451
+ bodySections: parseDocumentBody(task.body),
452
+ };
453
+ }