@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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/__tests__/smoke.test.d.ts +7 -0
- package/dist/__tests__/smoke.test.d.ts.map +1 -0
- package/dist/__tests__/smoke.test.js +132 -0
- package/dist/__tests__/smoke.test.js.map +1 -0
- package/dist/common/config.d.ts +30 -0
- package/dist/common/config.d.ts.map +1 -0
- package/dist/common/config.js +120 -0
- package/dist/common/config.js.map +1 -0
- package/dist/common/errors.d.ts +264 -0
- package/dist/common/errors.d.ts.map +1 -0
- package/dist/common/errors.js +491 -0
- package/dist/common/errors.js.map +1 -0
- package/dist/common/index.d.ts +9 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +25 -0
- package/dist/common/index.js.map +1 -0
- package/dist/common/types.d.ts +622 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/common/types.js +8 -0
- package/dist/common/types.js.map +1 -0
- package/dist/docs/index.d.ts +8 -0
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +26 -0
- package/dist/docs/index.js.map +1 -0
- package/dist/docs/manager.d.ts +47 -0
- package/dist/docs/manager.d.ts.map +1 -0
- package/dist/docs/manager.js +250 -0
- package/dist/docs/manager.js.map +1 -0
- package/dist/docs/types.d.ts +113 -0
- package/dist/docs/types.d.ts.map +1 -0
- package/dist/docs/types.js +8 -0
- package/dist/docs/types.js.map +1 -0
- package/dist/file/index.d.ts +9 -0
- package/dist/file/index.d.ts.map +1 -0
- package/dist/file/index.js +28 -0
- package/dist/file/index.js.map +1 -0
- package/dist/file/local.d.ts +42 -0
- package/dist/file/local.d.ts.map +1 -0
- package/dist/file/local.js +137 -0
- package/dist/file/local.js.map +1 -0
- package/dist/file/manager.d.ts +42 -0
- package/dist/file/manager.d.ts.map +1 -0
- package/dist/file/manager.js +68 -0
- package/dist/file/manager.js.map +1 -0
- package/dist/file/types.d.ts +52 -0
- package/dist/file/types.d.ts.map +1 -0
- package/dist/file/types.js +8 -0
- package/dist/file/types.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/logs/index.d.ts +8 -0
- package/dist/logs/index.d.ts.map +1 -0
- package/dist/logs/index.js +26 -0
- package/dist/logs/index.js.map +1 -0
- package/dist/logs/manager.d.ts +97 -0
- package/dist/logs/manager.d.ts.map +1 -0
- package/dist/logs/manager.js +584 -0
- package/dist/logs/manager.js.map +1 -0
- package/dist/logs/types.d.ts +45 -0
- package/dist/logs/types.d.ts.map +1 -0
- package/dist/logs/types.js +8 -0
- package/dist/logs/types.js.map +1 -0
- package/dist/repo/git.d.ts +182 -0
- package/dist/repo/git.d.ts.map +1 -0
- package/dist/repo/git.js +496 -0
- package/dist/repo/git.js.map +1 -0
- package/dist/repo/index.d.ts +10 -0
- package/dist/repo/index.d.ts.map +1 -0
- package/dist/repo/index.js +29 -0
- package/dist/repo/index.js.map +1 -0
- package/dist/repo/manager.d.ts +179 -0
- package/dist/repo/manager.d.ts.map +1 -0
- package/dist/repo/manager.js +433 -0
- package/dist/repo/manager.js.map +1 -0
- package/dist/repo/providers/bitbucket.d.ts +48 -0
- package/dist/repo/providers/bitbucket.d.ts.map +1 -0
- package/dist/repo/providers/bitbucket.js +86 -0
- package/dist/repo/providers/bitbucket.js.map +1 -0
- package/dist/repo/providers/github.d.ts +30 -0
- package/dist/repo/providers/github.d.ts.map +1 -0
- package/dist/repo/providers/github.js +311 -0
- package/dist/repo/providers/github.js.map +1 -0
- package/dist/repo/providers/gitlab.d.ts +47 -0
- package/dist/repo/providers/gitlab.d.ts.map +1 -0
- package/dist/repo/providers/gitlab.js +84 -0
- package/dist/repo/providers/gitlab.js.map +1 -0
- package/dist/repo/providers/index.d.ts +9 -0
- package/dist/repo/providers/index.d.ts.map +1 -0
- package/dist/repo/providers/index.js +15 -0
- package/dist/repo/providers/index.js.map +1 -0
- package/dist/repo/types.d.ts +48 -0
- package/dist/repo/types.d.ts.map +1 -0
- package/dist/repo/types.js +8 -0
- package/dist/repo/types.js.map +1 -0
- package/dist/spec/index.d.ts +9 -0
- package/dist/spec/index.d.ts.map +1 -0
- package/dist/spec/index.js +30 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/spec/manager.d.ts +106 -0
- package/dist/spec/manager.d.ts.map +1 -0
- package/dist/spec/manager.js +672 -0
- package/dist/spec/manager.js.map +1 -0
- package/dist/spec/templates.d.ts +28 -0
- package/dist/spec/templates.d.ts.map +1 -0
- package/dist/spec/templates.js +357 -0
- package/dist/spec/templates.js.map +1 -0
- package/dist/spec/types.d.ts +53 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +8 -0
- package/dist/spec/types.js.map +1 -0
- package/dist/work/index.d.ts +8 -0
- package/dist/work/index.d.ts.map +1 -0
- package/dist/work/index.js +26 -0
- package/dist/work/index.js.map +1 -0
- package/dist/work/manager.d.ts +112 -0
- package/dist/work/manager.d.ts.map +1 -0
- package/dist/work/manager.js +227 -0
- package/dist/work/manager.js.map +1 -0
- package/dist/work/providers/github.d.ts +40 -0
- package/dist/work/providers/github.d.ts.map +1 -0
- package/dist/work/providers/github.js +299 -0
- package/dist/work/providers/github.js.map +1 -0
- package/dist/work/providers/jira.d.ts +60 -0
- package/dist/work/providers/jira.d.ts.map +1 -0
- package/dist/work/providers/jira.js +109 -0
- package/dist/work/providers/jira.js.map +1 -0
- package/dist/work/providers/linear.d.ts +57 -0
- package/dist/work/providers/linear.d.ts.map +1 -0
- package/dist/work/providers/linear.js +103 -0
- package/dist/work/providers/linear.js.map +1 -0
- package/dist/work/types.d.ts +42 -0
- package/dist/work/types.d.ts.map +1 -0
- package/dist/work/types.js +8 -0
- package/dist/work/types.js.map +1 -0
- 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
|