@fractary/core 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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/__tests__/smoke.test.d.ts +7 -0
  4. package/dist/__tests__/smoke.test.d.ts.map +1 -0
  5. package/dist/__tests__/smoke.test.js +132 -0
  6. package/dist/__tests__/smoke.test.js.map +1 -0
  7. package/dist/common/config.d.ts +30 -0
  8. package/dist/common/config.d.ts.map +1 -0
  9. package/dist/common/config.js +120 -0
  10. package/dist/common/config.js.map +1 -0
  11. package/dist/common/errors.d.ts +264 -0
  12. package/dist/common/errors.d.ts.map +1 -0
  13. package/dist/common/errors.js +491 -0
  14. package/dist/common/errors.js.map +1 -0
  15. package/dist/common/index.d.ts +9 -0
  16. package/dist/common/index.d.ts.map +1 -0
  17. package/dist/common/index.js +25 -0
  18. package/dist/common/index.js.map +1 -0
  19. package/dist/common/types.d.ts +622 -0
  20. package/dist/common/types.d.ts.map +1 -0
  21. package/dist/common/types.js +8 -0
  22. package/dist/common/types.js.map +1 -0
  23. package/dist/docs/index.d.ts +8 -0
  24. package/dist/docs/index.d.ts.map +1 -0
  25. package/dist/docs/index.js +26 -0
  26. package/dist/docs/index.js.map +1 -0
  27. package/dist/docs/manager.d.ts +47 -0
  28. package/dist/docs/manager.d.ts.map +1 -0
  29. package/dist/docs/manager.js +250 -0
  30. package/dist/docs/manager.js.map +1 -0
  31. package/dist/docs/types.d.ts +113 -0
  32. package/dist/docs/types.d.ts.map +1 -0
  33. package/dist/docs/types.js +8 -0
  34. package/dist/docs/types.js.map +1 -0
  35. package/dist/file/index.d.ts +9 -0
  36. package/dist/file/index.d.ts.map +1 -0
  37. package/dist/file/index.js +28 -0
  38. package/dist/file/index.js.map +1 -0
  39. package/dist/file/local.d.ts +42 -0
  40. package/dist/file/local.d.ts.map +1 -0
  41. package/dist/file/local.js +137 -0
  42. package/dist/file/local.js.map +1 -0
  43. package/dist/file/manager.d.ts +42 -0
  44. package/dist/file/manager.d.ts.map +1 -0
  45. package/dist/file/manager.js +68 -0
  46. package/dist/file/manager.js.map +1 -0
  47. package/dist/file/types.d.ts +52 -0
  48. package/dist/file/types.d.ts.map +1 -0
  49. package/dist/file/types.js +8 -0
  50. package/dist/file/types.js.map +1 -0
  51. package/dist/index.d.ts +21 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +43 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/logs/index.d.ts +8 -0
  56. package/dist/logs/index.d.ts.map +1 -0
  57. package/dist/logs/index.js +26 -0
  58. package/dist/logs/index.js.map +1 -0
  59. package/dist/logs/manager.d.ts +97 -0
  60. package/dist/logs/manager.d.ts.map +1 -0
  61. package/dist/logs/manager.js +584 -0
  62. package/dist/logs/manager.js.map +1 -0
  63. package/dist/logs/types.d.ts +45 -0
  64. package/dist/logs/types.d.ts.map +1 -0
  65. package/dist/logs/types.js +8 -0
  66. package/dist/logs/types.js.map +1 -0
  67. package/dist/repo/git.d.ts +182 -0
  68. package/dist/repo/git.d.ts.map +1 -0
  69. package/dist/repo/git.js +496 -0
  70. package/dist/repo/git.js.map +1 -0
  71. package/dist/repo/index.d.ts +10 -0
  72. package/dist/repo/index.d.ts.map +1 -0
  73. package/dist/repo/index.js +29 -0
  74. package/dist/repo/index.js.map +1 -0
  75. package/dist/repo/manager.d.ts +179 -0
  76. package/dist/repo/manager.d.ts.map +1 -0
  77. package/dist/repo/manager.js +433 -0
  78. package/dist/repo/manager.js.map +1 -0
  79. package/dist/repo/providers/bitbucket.d.ts +48 -0
  80. package/dist/repo/providers/bitbucket.d.ts.map +1 -0
  81. package/dist/repo/providers/bitbucket.js +86 -0
  82. package/dist/repo/providers/bitbucket.js.map +1 -0
  83. package/dist/repo/providers/github.d.ts +30 -0
  84. package/dist/repo/providers/github.d.ts.map +1 -0
  85. package/dist/repo/providers/github.js +311 -0
  86. package/dist/repo/providers/github.js.map +1 -0
  87. package/dist/repo/providers/gitlab.d.ts +47 -0
  88. package/dist/repo/providers/gitlab.d.ts.map +1 -0
  89. package/dist/repo/providers/gitlab.js +84 -0
  90. package/dist/repo/providers/gitlab.js.map +1 -0
  91. package/dist/repo/providers/index.d.ts +9 -0
  92. package/dist/repo/providers/index.d.ts.map +1 -0
  93. package/dist/repo/providers/index.js +15 -0
  94. package/dist/repo/providers/index.js.map +1 -0
  95. package/dist/repo/types.d.ts +48 -0
  96. package/dist/repo/types.d.ts.map +1 -0
  97. package/dist/repo/types.js +8 -0
  98. package/dist/repo/types.js.map +1 -0
  99. package/dist/spec/index.d.ts +9 -0
  100. package/dist/spec/index.d.ts.map +1 -0
  101. package/dist/spec/index.js +30 -0
  102. package/dist/spec/index.js.map +1 -0
  103. package/dist/spec/manager.d.ts +106 -0
  104. package/dist/spec/manager.d.ts.map +1 -0
  105. package/dist/spec/manager.js +672 -0
  106. package/dist/spec/manager.js.map +1 -0
  107. package/dist/spec/templates.d.ts +28 -0
  108. package/dist/spec/templates.d.ts.map +1 -0
  109. package/dist/spec/templates.js +357 -0
  110. package/dist/spec/templates.js.map +1 -0
  111. package/dist/spec/types.d.ts +53 -0
  112. package/dist/spec/types.d.ts.map +1 -0
  113. package/dist/spec/types.js +8 -0
  114. package/dist/spec/types.js.map +1 -0
  115. package/dist/work/index.d.ts +8 -0
  116. package/dist/work/index.d.ts.map +1 -0
  117. package/dist/work/index.js +26 -0
  118. package/dist/work/index.js.map +1 -0
  119. package/dist/work/manager.d.ts +112 -0
  120. package/dist/work/manager.d.ts.map +1 -0
  121. package/dist/work/manager.js +227 -0
  122. package/dist/work/manager.js.map +1 -0
  123. package/dist/work/providers/github.d.ts +40 -0
  124. package/dist/work/providers/github.d.ts.map +1 -0
  125. package/dist/work/providers/github.js +299 -0
  126. package/dist/work/providers/github.js.map +1 -0
  127. package/dist/work/providers/jira.d.ts +60 -0
  128. package/dist/work/providers/jira.d.ts.map +1 -0
  129. package/dist/work/providers/jira.js +109 -0
  130. package/dist/work/providers/jira.js.map +1 -0
  131. package/dist/work/providers/linear.d.ts +57 -0
  132. package/dist/work/providers/linear.d.ts.map +1 -0
  133. package/dist/work/providers/linear.js +103 -0
  134. package/dist/work/providers/linear.js.map +1 -0
  135. package/dist/work/types.d.ts +42 -0
  136. package/dist/work/types.d.ts.map +1 -0
  137. package/dist/work/types.js +8 -0
  138. package/dist/work/types.js.map +1 -0
  139. package/package.json +102 -0
@@ -0,0 +1,672 @@
1
+ "use strict";
2
+ /**
3
+ * @fractary/faber - Spec Manager
4
+ *
5
+ * Specification management for FABER workflows.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.SpecManager = void 0;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const templates_1 = require("./templates");
45
+ const config_1 = require("../common/config");
46
+ const errors_1 = require("../common/errors");
47
+ /**
48
+ * Parse spec frontmatter and content
49
+ */
50
+ function parseSpec(content, filePath) {
51
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
52
+ if (!frontmatterMatch) {
53
+ throw new errors_1.SpecError('parse', `Invalid spec format in ${filePath}: missing frontmatter`);
54
+ }
55
+ const [, frontmatterStr, body] = frontmatterMatch;
56
+ const frontmatter = {};
57
+ for (const line of frontmatterStr.split('\n')) {
58
+ const match = line.match(/^(\w+):\s*(.*)$/);
59
+ if (match) {
60
+ const [, key, value] = match;
61
+ frontmatter[key] = value.replace(/^["']|["']$/g, '');
62
+ }
63
+ }
64
+ // Parse phases from content
65
+ const phases = parsePhases(body);
66
+ return {
67
+ id: frontmatter.id || path.basename(filePath, '.md'),
68
+ path: filePath,
69
+ title: frontmatter.title || 'Untitled',
70
+ workId: frontmatter.work_id,
71
+ workType: frontmatter.work_type || 'feature',
72
+ template: frontmatter.template || 'basic',
73
+ content: body,
74
+ metadata: {
75
+ created_at: frontmatter.created_at || new Date().toISOString(),
76
+ updated_at: frontmatter.updated_at || new Date().toISOString(),
77
+ validation_status: frontmatter.validation_status || 'not_validated',
78
+ source: frontmatter.source || 'conversation',
79
+ },
80
+ phases,
81
+ };
82
+ }
83
+ /**
84
+ * Parse phases from spec content
85
+ */
86
+ function parsePhases(content) {
87
+ const phases = [];
88
+ const phaseMatches = content.matchAll(/## Phase (\d+): ([^\n]+)\n([\s\S]*?)(?=## Phase \d+:|## [A-Z]|$)/gi);
89
+ for (const match of phaseMatches) {
90
+ const [, phaseNum, title, phaseContent] = match;
91
+ const tasks = parseTasks(phaseContent);
92
+ // Determine status based on tasks
93
+ let status = 'not_started';
94
+ if (tasks.length > 0) {
95
+ const completedCount = tasks.filter(t => t.completed).length;
96
+ if (completedCount === tasks.length) {
97
+ status = 'complete';
98
+ }
99
+ else if (completedCount > 0) {
100
+ status = 'in_progress';
101
+ }
102
+ }
103
+ phases.push({
104
+ id: `phase-${phaseNum}`,
105
+ title: title.trim(),
106
+ status,
107
+ tasks,
108
+ });
109
+ }
110
+ return phases;
111
+ }
112
+ /**
113
+ * Parse tasks from phase content
114
+ */
115
+ function parseTasks(content) {
116
+ const tasks = [];
117
+ const taskMatches = content.matchAll(/- \[([ xX])\] (.+)/g);
118
+ for (const match of taskMatches) {
119
+ const [, checkbox, text] = match;
120
+ tasks.push({
121
+ text: text.trim(),
122
+ completed: checkbox.toLowerCase() === 'x',
123
+ });
124
+ }
125
+ return tasks;
126
+ }
127
+ /**
128
+ * Serialize spec back to markdown
129
+ */
130
+ function serializeSpec(spec) {
131
+ const lines = [];
132
+ // Frontmatter
133
+ lines.push('---');
134
+ lines.push(`id: ${spec.id}`);
135
+ lines.push(`title: "${spec.title}"`);
136
+ if (spec.workId) {
137
+ lines.push(`work_id: "${spec.workId}"`);
138
+ }
139
+ lines.push(`work_type: ${spec.workType}`);
140
+ lines.push(`template: ${spec.template}`);
141
+ lines.push(`created_at: ${spec.metadata.created_at}`);
142
+ lines.push(`updated_at: ${new Date().toISOString()}`);
143
+ if (spec.metadata.validation_status) {
144
+ lines.push(`validation_status: ${spec.metadata.validation_status}`);
145
+ }
146
+ lines.push(`source: ${spec.metadata.source}`);
147
+ lines.push('---');
148
+ lines.push('');
149
+ // Content
150
+ lines.push(spec.content);
151
+ return lines.join('\n');
152
+ }
153
+ /**
154
+ * Specification Manager
155
+ */
156
+ class SpecManager {
157
+ config;
158
+ specsDir;
159
+ constructor(config) {
160
+ // Try to load config, but allow missing - use defaults if not found
161
+ const loadedConfig = config ? null : (0, config_1.loadSpecConfig)(undefined, { allowMissing: true });
162
+ // Merge provided config, loaded config, or use defaults
163
+ this.config = this.mergeWithDefaults(config, loadedConfig);
164
+ const projectRoot = (0, config_1.findProjectRoot)();
165
+ this.specsDir = this.config.localPath || path.join(projectRoot, 'specs');
166
+ }
167
+ /**
168
+ * Get default spec configuration
169
+ */
170
+ getDefaultSpecConfig() {
171
+ const projectRoot = (0, config_1.findProjectRoot)();
172
+ return {
173
+ localPath: path.join(projectRoot, 'specs'),
174
+ };
175
+ }
176
+ /**
177
+ * Merge partial config with defaults
178
+ */
179
+ mergeWithDefaults(partialConfig, loadedConfig) {
180
+ const defaults = this.getDefaultSpecConfig();
181
+ // Priority: provided config > loaded config > defaults
182
+ return {
183
+ localPath: partialConfig?.localPath || loadedConfig?.localPath || defaults.localPath,
184
+ };
185
+ }
186
+ /**
187
+ * Ensure specs directory exists
188
+ */
189
+ ensureSpecsDir() {
190
+ if (!fs.existsSync(this.specsDir)) {
191
+ fs.mkdirSync(this.specsDir, { recursive: true });
192
+ }
193
+ }
194
+ /**
195
+ * Get spec file path
196
+ */
197
+ getSpecPath(id) {
198
+ // If it's already a full path, return it
199
+ if (path.isAbsolute(id)) {
200
+ return id;
201
+ }
202
+ // If it has a .md extension, treat as filename
203
+ if (id.endsWith('.md')) {
204
+ return path.join(this.specsDir, id);
205
+ }
206
+ // Otherwise, construct path from ID
207
+ return path.join(this.specsDir, `${id}.md`);
208
+ }
209
+ // =========================================================================
210
+ // CRUD Operations
211
+ // =========================================================================
212
+ /**
213
+ * Create a new specification
214
+ */
215
+ createSpec(title, options) {
216
+ this.ensureSpecsDir();
217
+ const templateType = options?.template || 'basic';
218
+ const template = (0, templates_1.getTemplate)(templateType);
219
+ const workType = this.inferWorkType(templateType);
220
+ const content = (0, templates_1.generateSpecContent)(template, {
221
+ title,
222
+ workId: options?.workId,
223
+ workType,
224
+ context: options?.context,
225
+ });
226
+ // Generate filename
227
+ const slug = title
228
+ .toLowerCase()
229
+ .replace(/[^a-z0-9]+/g, '-')
230
+ .replace(/^-|-$/g, '')
231
+ .slice(0, 50);
232
+ const filename = options?.workId
233
+ ? `${options.workId}-${slug}.md`
234
+ : `${slug}.md`;
235
+ const filePath = path.join(this.specsDir, filename);
236
+ // Check if exists
237
+ if (fs.existsSync(filePath) && !options?.force) {
238
+ throw new errors_1.SpecError('create', `Spec already exists at ${filePath}. Use force option to overwrite.`);
239
+ }
240
+ // Write file
241
+ fs.writeFileSync(filePath, content, 'utf-8');
242
+ // Return parsed spec
243
+ return parseSpec(content, filePath);
244
+ }
245
+ /**
246
+ * Get a specification by ID or path
247
+ */
248
+ getSpec(idOrPath) {
249
+ const filePath = this.getSpecPath(idOrPath);
250
+ if (!fs.existsSync(filePath)) {
251
+ return null;
252
+ }
253
+ const content = fs.readFileSync(filePath, 'utf-8');
254
+ return parseSpec(content, filePath);
255
+ }
256
+ /**
257
+ * Update a specification
258
+ */
259
+ updateSpec(idOrPath, updates) {
260
+ const spec = this.getSpec(idOrPath);
261
+ if (!spec) {
262
+ throw new errors_1.SpecError('update', `Spec not found: ${idOrPath}`);
263
+ }
264
+ // Apply updates
265
+ if (updates.title !== undefined) {
266
+ spec.title = updates.title;
267
+ }
268
+ if (updates.content !== undefined) {
269
+ spec.content = updates.content;
270
+ }
271
+ if (updates.workId !== undefined) {
272
+ spec.workId = updates.workId;
273
+ }
274
+ if (updates.workType !== undefined) {
275
+ spec.workType = updates.workType;
276
+ }
277
+ if (updates.validationStatus !== undefined) {
278
+ spec.metadata.validation_status = updates.validationStatus;
279
+ }
280
+ // Update timestamp
281
+ spec.metadata.updated_at = new Date().toISOString();
282
+ // Write back
283
+ fs.writeFileSync(spec.path, serializeSpec(spec), 'utf-8');
284
+ return spec;
285
+ }
286
+ /**
287
+ * Delete a specification
288
+ */
289
+ deleteSpec(idOrPath) {
290
+ const filePath = this.getSpecPath(idOrPath);
291
+ if (!fs.existsSync(filePath)) {
292
+ return false;
293
+ }
294
+ fs.unlinkSync(filePath);
295
+ return true;
296
+ }
297
+ /**
298
+ * List all specifications
299
+ */
300
+ listSpecs(options) {
301
+ this.ensureSpecsDir();
302
+ const files = fs.readdirSync(this.specsDir).filter(f => f.endsWith('.md'));
303
+ const specs = [];
304
+ for (const file of files) {
305
+ try {
306
+ const filePath = path.join(this.specsDir, file);
307
+ const content = fs.readFileSync(filePath, 'utf-8');
308
+ const spec = parseSpec(content, filePath);
309
+ // Apply filters
310
+ if (options?.workId && spec.workId !== options.workId) {
311
+ continue;
312
+ }
313
+ if (options?.template && spec.template !== options.template) {
314
+ continue;
315
+ }
316
+ if (options?.status) {
317
+ const status = spec.metadata.validation_status || 'not_validated';
318
+ if (status !== options.status) {
319
+ continue;
320
+ }
321
+ }
322
+ specs.push(spec);
323
+ }
324
+ catch {
325
+ // Skip invalid spec files
326
+ }
327
+ }
328
+ return specs;
329
+ }
330
+ // =========================================================================
331
+ // Phase & Task Operations
332
+ // =========================================================================
333
+ /**
334
+ * Update a phase in a specification
335
+ */
336
+ updatePhase(specIdOrPath, phaseId, updates) {
337
+ const spec = this.getSpec(specIdOrPath);
338
+ if (!spec) {
339
+ throw new errors_1.SpecError('updatePhase', `Spec not found: ${specIdOrPath}`);
340
+ }
341
+ const phase = spec.phases?.find(p => p.id === phaseId);
342
+ if (!phase) {
343
+ throw new errors_1.SpecError('updatePhase', `Phase not found: ${phaseId}`);
344
+ }
345
+ // Apply updates
346
+ if (updates.status !== undefined) {
347
+ phase.status = updates.status;
348
+ }
349
+ if (updates.objective !== undefined) {
350
+ phase.objective = updates.objective;
351
+ }
352
+ if (updates.notes !== undefined) {
353
+ phase.notes = updates.notes;
354
+ }
355
+ // Update content to reflect phase changes
356
+ spec.content = this.updatePhaseInContent(spec.content, phase);
357
+ spec.metadata.updated_at = new Date().toISOString();
358
+ // Write back
359
+ fs.writeFileSync(spec.path, serializeSpec(spec), 'utf-8');
360
+ return spec;
361
+ }
362
+ /**
363
+ * Complete a task in a phase
364
+ */
365
+ completeTask(specIdOrPath, phaseId, taskIndex) {
366
+ const spec = this.getSpec(specIdOrPath);
367
+ if (!spec) {
368
+ throw new errors_1.SpecError('completeTask', `Spec not found: ${specIdOrPath}`);
369
+ }
370
+ const phase = spec.phases?.find(p => p.id === phaseId);
371
+ if (!phase) {
372
+ throw new errors_1.SpecError('completeTask', `Phase not found: ${phaseId}`);
373
+ }
374
+ if (taskIndex < 0 || taskIndex >= phase.tasks.length) {
375
+ throw new errors_1.SpecError('completeTask', `Invalid task index: ${taskIndex}`);
376
+ }
377
+ phase.tasks[taskIndex].completed = true;
378
+ // Update phase status
379
+ const completedCount = phase.tasks.filter(t => t.completed).length;
380
+ if (completedCount === phase.tasks.length) {
381
+ phase.status = 'complete';
382
+ }
383
+ else if (completedCount > 0) {
384
+ phase.status = 'in_progress';
385
+ }
386
+ // Update content
387
+ spec.content = this.updateTasksInContent(spec.content, phase);
388
+ spec.metadata.updated_at = new Date().toISOString();
389
+ // Write back
390
+ fs.writeFileSync(spec.path, serializeSpec(spec), 'utf-8');
391
+ return spec;
392
+ }
393
+ /**
394
+ * Add a task to a phase
395
+ */
396
+ addTask(specIdOrPath, phaseId, taskText) {
397
+ const spec = this.getSpec(specIdOrPath);
398
+ if (!spec) {
399
+ throw new errors_1.SpecError('addTask', `Spec not found: ${specIdOrPath}`);
400
+ }
401
+ const phase = spec.phases?.find(p => p.id === phaseId);
402
+ if (!phase) {
403
+ throw new errors_1.SpecError('addTask', `Phase not found: ${phaseId}`);
404
+ }
405
+ phase.tasks.push({
406
+ text: taskText,
407
+ completed: false,
408
+ });
409
+ // Update content
410
+ spec.content = this.updateTasksInContent(spec.content, phase);
411
+ spec.metadata.updated_at = new Date().toISOString();
412
+ // Write back
413
+ fs.writeFileSync(spec.path, serializeSpec(spec), 'utf-8');
414
+ return spec;
415
+ }
416
+ // =========================================================================
417
+ // Validation
418
+ // =========================================================================
419
+ /**
420
+ * Validate a specification
421
+ */
422
+ validateSpec(specIdOrPath) {
423
+ const spec = this.getSpec(specIdOrPath);
424
+ if (!spec) {
425
+ throw new errors_1.SpecError('validate', `Spec not found: ${specIdOrPath}`);
426
+ }
427
+ const checks = {
428
+ requirements: { completed: 0, total: 0, status: 'pass' },
429
+ acceptanceCriteria: { met: 0, total: 0, status: 'pass' },
430
+ filesModified: { status: 'pass' },
431
+ testsAdded: { added: 0, expected: 0, status: 'pass' },
432
+ docsUpdated: { status: 'pass' },
433
+ };
434
+ const suggestions = [];
435
+ // Check requirements section
436
+ const requirementsMatch = spec.content.match(/## Requirements\n([\s\S]*?)(?=##|$)/i);
437
+ if (requirementsMatch) {
438
+ const reqContent = requirementsMatch[1];
439
+ const allReqs = reqContent.match(/- \[([ xX])\]/g) || [];
440
+ const completedReqs = reqContent.match(/- \[[xX]\]/g) || [];
441
+ checks.requirements.total = allReqs.length;
442
+ checks.requirements.completed = completedReqs.length;
443
+ if (checks.requirements.total === 0) {
444
+ checks.requirements.status = 'fail';
445
+ suggestions.push('Add specific requirements to the Requirements section');
446
+ }
447
+ else if (checks.requirements.completed < checks.requirements.total) {
448
+ checks.requirements.status = 'warn';
449
+ suggestions.push(`Complete remaining requirements (${checks.requirements.total - checks.requirements.completed} pending)`);
450
+ }
451
+ }
452
+ else {
453
+ checks.requirements.status = 'fail';
454
+ suggestions.push('Add a Requirements section to the specification');
455
+ }
456
+ // Check acceptance criteria
457
+ const acMatch = spec.content.match(/## Acceptance Criteria\n([\s\S]*?)(?=##|$)/i);
458
+ if (acMatch) {
459
+ const acContent = acMatch[1];
460
+ const allAC = acContent.match(/- \[([ xX])\]/g) || [];
461
+ const metAC = acContent.match(/- \[[xX]\]/g) || [];
462
+ checks.acceptanceCriteria.total = allAC.length;
463
+ checks.acceptanceCriteria.met = metAC.length;
464
+ if (checks.acceptanceCriteria.total === 0) {
465
+ checks.acceptanceCriteria.status = 'fail';
466
+ suggestions.push('Add specific acceptance criteria');
467
+ }
468
+ else if (checks.acceptanceCriteria.met < checks.acceptanceCriteria.total) {
469
+ checks.acceptanceCriteria.status = 'warn';
470
+ }
471
+ }
472
+ else {
473
+ checks.acceptanceCriteria.status = 'fail';
474
+ suggestions.push('Add an Acceptance Criteria section');
475
+ }
476
+ // Check testing section
477
+ const testMatch = spec.content.match(/## Testing|## Tests/i);
478
+ if (!testMatch) {
479
+ checks.testsAdded.status = 'warn';
480
+ suggestions.push('Add a Testing section describing test coverage');
481
+ }
482
+ // Calculate overall status and score
483
+ let passCount = 0;
484
+ let warnCount = 0;
485
+ let failCount = 0;
486
+ for (const check of Object.values(checks)) {
487
+ if (check.status === 'pass')
488
+ passCount++;
489
+ else if (check.status === 'warn')
490
+ warnCount++;
491
+ else
492
+ failCount++;
493
+ }
494
+ let status;
495
+ if (failCount > 0) {
496
+ status = 'fail';
497
+ }
498
+ else if (warnCount > 0) {
499
+ status = 'partial';
500
+ }
501
+ else {
502
+ status = 'pass';
503
+ }
504
+ const totalChecks = passCount + warnCount + failCount;
505
+ const score = Math.round((passCount / totalChecks) * 100);
506
+ // Update spec validation status
507
+ this.updateSpec(specIdOrPath, {
508
+ validationStatus: status === 'pass' ? 'complete' : status === 'partial' ? 'partial' : 'failed',
509
+ });
510
+ return {
511
+ status,
512
+ score,
513
+ checks,
514
+ suggestions: suggestions.length > 0 ? suggestions : undefined,
515
+ };
516
+ }
517
+ // =========================================================================
518
+ // Refinement
519
+ // =========================================================================
520
+ /**
521
+ * Generate refinement questions for a spec
522
+ */
523
+ generateRefinementQuestions(specIdOrPath) {
524
+ const spec = this.getSpec(specIdOrPath);
525
+ if (!spec) {
526
+ throw new errors_1.SpecError('refine', `Spec not found: ${specIdOrPath}`);
527
+ }
528
+ const questions = [];
529
+ // Check for missing sections based on template
530
+ const template = (0, templates_1.getTemplate)(spec.template);
531
+ for (const section of template.sections) {
532
+ const sectionRegex = new RegExp(`## ${section.title}`, 'i');
533
+ if (!sectionRegex.test(spec.content)) {
534
+ questions.push({
535
+ id: `missing-${section.id}`,
536
+ question: `The "${section.title}" section is missing. ${section.description}`,
537
+ category: 'structure',
538
+ priority: section.required ? 'high' : 'medium',
539
+ });
540
+ }
541
+ }
542
+ // Check for empty sections
543
+ const emptySectionMatch = spec.content.match(/## ([^\n]+)\n\n(<!--[^>]*-->)?\n*(?=##|$)/g);
544
+ if (emptySectionMatch) {
545
+ for (const match of emptySectionMatch) {
546
+ const titleMatch = match.match(/## ([^\n]+)/);
547
+ if (titleMatch) {
548
+ questions.push({
549
+ id: `empty-${titleMatch[1].toLowerCase().replace(/\s+/g, '-')}`,
550
+ question: `The "${titleMatch[1]}" section appears to be empty. Please add content.`,
551
+ category: 'content',
552
+ priority: 'medium',
553
+ });
554
+ }
555
+ }
556
+ }
557
+ // Check for vague requirements
558
+ const vaguePatterns = ['something', 'somehow', 'maybe', 'probably', 'etc', 'tbd', 'todo'];
559
+ for (const pattern of vaguePatterns) {
560
+ if (spec.content.toLowerCase().includes(pattern)) {
561
+ questions.push({
562
+ id: `vague-${pattern}`,
563
+ question: `The spec contains vague language ("${pattern}"). Please be more specific.`,
564
+ category: 'clarity',
565
+ priority: 'low',
566
+ });
567
+ break; // Only add one vague language warning
568
+ }
569
+ }
570
+ return questions;
571
+ }
572
+ /**
573
+ * Apply refinements to a spec
574
+ */
575
+ refineSpec(specIdOrPath, answers) {
576
+ const spec = this.getSpec(specIdOrPath);
577
+ if (!spec) {
578
+ throw new errors_1.SpecError('refine', `Spec not found: ${specIdOrPath}`);
579
+ }
580
+ let improvementsApplied = 0;
581
+ // Apply answers as notes/content updates
582
+ for (const [_questionId, answer] of Object.entries(answers)) {
583
+ if (answer && answer.trim()) {
584
+ // Add answer as a note in the relevant section
585
+ // This is a simple implementation - could be enhanced
586
+ improvementsApplied++;
587
+ }
588
+ }
589
+ // Update metadata
590
+ spec.metadata.updated_at = new Date().toISOString();
591
+ fs.writeFileSync(spec.path, serializeSpec(spec), 'utf-8');
592
+ // Generate new questions to see if more refinement is needed
593
+ const newQuestions = this.generateRefinementQuestions(specIdOrPath);
594
+ const highPriorityRemaining = newQuestions.filter(q => q.priority === 'high').length;
595
+ return {
596
+ questionsAsked: Object.keys(answers).length,
597
+ questionsAnswered: Object.values(answers).filter(a => a && a.trim()).length,
598
+ improvementsApplied,
599
+ additionalRoundsRecommended: highPriorityRemaining > 0,
600
+ };
601
+ }
602
+ // =========================================================================
603
+ // Helpers
604
+ // =========================================================================
605
+ /**
606
+ * Infer work type from template type
607
+ */
608
+ inferWorkType(templateType) {
609
+ switch (templateType) {
610
+ case 'feature':
611
+ return 'feature';
612
+ case 'bug':
613
+ return 'bug';
614
+ case 'infrastructure':
615
+ return 'chore';
616
+ case 'api':
617
+ return 'feature';
618
+ default:
619
+ return 'feature';
620
+ }
621
+ }
622
+ /**
623
+ * Update phase content in spec
624
+ */
625
+ updatePhaseInContent(content, phase) {
626
+ // Find and replace the phase section
627
+ const phaseNum = phase.id.replace('phase-', '');
628
+ const phaseRegex = new RegExp(`(## Phase ${phaseNum}: [^\\n]+\\n)([\\s\\S]*?)(?=## Phase \\d+:|## [A-Z]|$)`, 'i');
629
+ const match = content.match(phaseRegex);
630
+ if (!match) {
631
+ return content;
632
+ }
633
+ // Build new phase content
634
+ const lines = [];
635
+ if (phase.objective) {
636
+ lines.push(`\n**Objective:** ${phase.objective}\n`);
637
+ }
638
+ if (phase.status !== 'not_started') {
639
+ lines.push(`\n**Status:** ${phase.status}\n`);
640
+ }
641
+ lines.push('\n');
642
+ for (const task of phase.tasks) {
643
+ const checkbox = task.completed ? '[x]' : '[ ]';
644
+ lines.push(`- ${checkbox} ${task.text}\n`);
645
+ }
646
+ if (phase.notes && phase.notes.length > 0) {
647
+ lines.push('\n**Notes:**\n');
648
+ for (const note of phase.notes) {
649
+ lines.push(`- ${note}\n`);
650
+ }
651
+ }
652
+ return content.replace(phaseRegex, `$1${lines.join('')}`);
653
+ }
654
+ /**
655
+ * Update tasks in content
656
+ */
657
+ updateTasksInContent(content, phase) {
658
+ return this.updatePhaseInContent(content, phase);
659
+ }
660
+ /**
661
+ * Get available templates
662
+ */
663
+ getTemplates() {
664
+ return Object.values(templates_1.templates).map(t => ({
665
+ id: t.id,
666
+ name: t.name,
667
+ description: t.description,
668
+ }));
669
+ }
670
+ }
671
+ exports.SpecManager = SpecManager;
672
+ //# sourceMappingURL=manager.js.map