spec-agent 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -0
- package/bin/spec-agent.js +14 -0
- package/dist/commands/analyze.d.ts +16 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +283 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/clean.d.ts +9 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +109 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/dispatch.d.ts +12 -0
- package/dist/commands/dispatch.d.ts.map +1 -0
- package/dist/commands/dispatch.js +232 -0
- package/dist/commands/dispatch.js.map +1 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +153 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/learn.d.ts +13 -0
- package/dist/commands/learn.d.ts.map +1 -0
- package/dist/commands/learn.js +234 -0
- package/dist/commands/learn.js.map +1 -0
- package/dist/commands/merge.d.ts +11 -0
- package/dist/commands/merge.d.ts.map +1 -0
- package/dist/commands/merge.js +335 -0
- package/dist/commands/merge.js.map +1 -0
- package/dist/commands/pipeline.d.ts +19 -0
- package/dist/commands/pipeline.d.ts.map +1 -0
- package/dist/commands/pipeline.js +266 -0
- package/dist/commands/pipeline.js.map +1 -0
- package/dist/commands/plan.d.ts +13 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +314 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/scan.d.ts +28 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +488 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +146 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/services/document-parser.d.ts +49 -0
- package/dist/services/document-parser.d.ts.map +1 -0
- package/dist/services/document-parser.js +499 -0
- package/dist/services/document-parser.js.map +1 -0
- package/dist/services/llm.d.ts +61 -0
- package/dist/services/llm.d.ts.map +1 -0
- package/dist/services/llm.js +716 -0
- package/dist/services/llm.js.map +1 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/file.d.ts +10 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +96 -0
- package/dist/utils/file.js.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +55 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +48 -0
- package/scripts/publish-npm.js +174 -0
- package/spec-agent-implementation.md +750 -0
- package/src/commands/analyze.ts +322 -0
- package/src/commands/clean.ts +88 -0
- package/src/commands/dispatch.ts +250 -0
- package/src/commands/doctor.ts +136 -0
- package/src/commands/learn.ts +261 -0
- package/src/commands/merge.ts +377 -0
- package/src/commands/pipeline.ts +306 -0
- package/src/commands/plan.ts +331 -0
- package/src/commands/scan.ts +568 -0
- package/src/commands/status.ts +129 -0
- package/src/index.ts +137 -0
- package/src/services/document-parser.ts +548 -0
- package/src/services/llm.ts +857 -0
- package/src/types.ts +161 -0
- package/src/utils/file.ts +60 -0
- package/src/utils/logger.ts +58 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { Logger } from '../utils/logger';
|
|
4
|
+
import {
|
|
5
|
+
ensureDir,
|
|
6
|
+
fileExists,
|
|
7
|
+
readJson,
|
|
8
|
+
writeJson
|
|
9
|
+
} from '../utils/file';
|
|
10
|
+
import { SpecSummary, TaskPlan, Task, ParallelGroup, Feature } from '../types';
|
|
11
|
+
|
|
12
|
+
interface PlanOptions {
|
|
13
|
+
spec: string;
|
|
14
|
+
output: string;
|
|
15
|
+
type: string;
|
|
16
|
+
framework: string;
|
|
17
|
+
parallel: string;
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
yes?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function planCommand(options: PlanOptions, command: Command): Promise<void> {
|
|
23
|
+
const logger = new Logger();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const specPath = path.resolve(options.spec);
|
|
27
|
+
|
|
28
|
+
if (!(await fileExists(specPath))) {
|
|
29
|
+
logger.error(`Error: Spec summary not found: ${options.spec}`);
|
|
30
|
+
logger.info('Run spec-agent merge first to create spec_summary.json.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const spec: SpecSummary = await readJson(specPath);
|
|
35
|
+
logger.info(`Loaded spec with ${spec.features.length} features, ${spec.pages.length} pages, ${spec.apis.length} APIs`);
|
|
36
|
+
|
|
37
|
+
// Preview mode
|
|
38
|
+
if (options.dryRun) {
|
|
39
|
+
logger.info('Dry run mode - task plan preview:');
|
|
40
|
+
logger.info(` Features to plan: ${spec.features.length}`);
|
|
41
|
+
logger.info(` Framework: ${options.framework}`);
|
|
42
|
+
logger.info(` Output type: ${options.type}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
|
|
48
|
+
// Generate tasks from spec
|
|
49
|
+
const tasks: Task[] = [];
|
|
50
|
+
const featureTaskIdMap = new Map<string, string>();
|
|
51
|
+
|
|
52
|
+
// 1. Setup task (always first)
|
|
53
|
+
tasks.push({
|
|
54
|
+
id: 'T001',
|
|
55
|
+
name: '项目基础架构初始化',
|
|
56
|
+
type: 'setup',
|
|
57
|
+
description: `Initialize ${options.framework} project structure`,
|
|
58
|
+
dependencies: [],
|
|
59
|
+
priority: 'P0',
|
|
60
|
+
estimatedHours: 2,
|
|
61
|
+
status: 'pending',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 2. Tasks from features
|
|
65
|
+
let taskId = 2;
|
|
66
|
+
const dedupedFeatures = deduplicateFeaturesForPlanning(spec.features);
|
|
67
|
+
for (const feature of dedupedFeatures) {
|
|
68
|
+
const featureTask: Task = {
|
|
69
|
+
id: `T${String(taskId).padStart(3, '0')}`,
|
|
70
|
+
name: feature.name,
|
|
71
|
+
type: mapFeatureToTaskType(feature),
|
|
72
|
+
description: feature.description,
|
|
73
|
+
dependencies: ['T001'],
|
|
74
|
+
priority: feature.priority,
|
|
75
|
+
estimatedHours: estimateHours(feature),
|
|
76
|
+
status: 'pending',
|
|
77
|
+
};
|
|
78
|
+
tasks.push(featureTask);
|
|
79
|
+
featureTaskIdMap.set(feature.id, featureTask.id);
|
|
80
|
+
taskId++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Patch feature dependencies after all feature tasks are created.
|
|
84
|
+
for (const task of tasks.filter(item => item.type === 'feature' || item.type === 'api' || item.type === 'page' || item.type === 'component')) {
|
|
85
|
+
const feature = dedupedFeatures.find(item => item.name === task.name && item.description === task.description);
|
|
86
|
+
if (!feature) continue;
|
|
87
|
+
const mappedDependencies = (feature.dependencies || [])
|
|
88
|
+
.map(depId => featureTaskIdMap.get(depId))
|
|
89
|
+
.filter((depId): depId is string => Boolean(depId));
|
|
90
|
+
task.dependencies = mappedDependencies.length > 0
|
|
91
|
+
? Array.from(new Set(['T001', ...mappedDependencies]))
|
|
92
|
+
: ['T001'];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. Tasks from pages
|
|
96
|
+
for (const page of deduplicatePagesForPlanning(spec.pages)) {
|
|
97
|
+
const pageTask: Task = {
|
|
98
|
+
id: `T${String(taskId).padStart(3, '0')}`,
|
|
99
|
+
name: page.name,
|
|
100
|
+
type: 'page',
|
|
101
|
+
description: `Page at ${page.route}`,
|
|
102
|
+
dependencies: ['T001'],
|
|
103
|
+
priority: 'P1',
|
|
104
|
+
estimatedHours: 4,
|
|
105
|
+
status: 'pending',
|
|
106
|
+
};
|
|
107
|
+
tasks.push(pageTask);
|
|
108
|
+
taskId++;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 4. Tasks from APIs
|
|
112
|
+
for (const api of deduplicateApisForPlanning(spec.apis)) {
|
|
113
|
+
const apiTask: Task = {
|
|
114
|
+
id: `T${String(taskId).padStart(3, '0')}`,
|
|
115
|
+
name: `${api.method} ${api.path}`,
|
|
116
|
+
type: 'api',
|
|
117
|
+
description: api.description,
|
|
118
|
+
dependencies: ['T001'],
|
|
119
|
+
priority: 'P1',
|
|
120
|
+
estimatedHours: 2,
|
|
121
|
+
status: 'pending',
|
|
122
|
+
};
|
|
123
|
+
tasks.push(apiTask);
|
|
124
|
+
taskId++;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 5. Test tasks
|
|
128
|
+
tasks.push({
|
|
129
|
+
id: `T${String(taskId).padStart(3, '0')}`,
|
|
130
|
+
name: '单元测试',
|
|
131
|
+
type: 'test',
|
|
132
|
+
description: 'Unit tests for all components',
|
|
133
|
+
dependencies: tasks.filter(t => t.type !== 'test' && t.type !== 'setup').map(t => t.id),
|
|
134
|
+
priority: 'P2',
|
|
135
|
+
estimatedHours: 8,
|
|
136
|
+
status: 'pending',
|
|
137
|
+
});
|
|
138
|
+
taskId++;
|
|
139
|
+
|
|
140
|
+
const compactedTasks = deduplicateTasks(tasks);
|
|
141
|
+
logger.info(`Generated ${tasks.length} tasks, compacted to ${compactedTasks.length} tasks`);
|
|
142
|
+
|
|
143
|
+
// Group tasks by dependencies (parallel groups)
|
|
144
|
+
const parallelGroups = groupTasksByDependencies(compactedTasks, parseInt(options.parallel, 10));
|
|
145
|
+
|
|
146
|
+
logger.info(`Organized into ${parallelGroups.length} parallel groups`);
|
|
147
|
+
|
|
148
|
+
// Create task plan
|
|
149
|
+
const taskPlan: TaskPlan = {
|
|
150
|
+
version: '1.0.0',
|
|
151
|
+
createdAt: new Date().toISOString(),
|
|
152
|
+
totalTasks: compactedTasks.length,
|
|
153
|
+
parallelGroups,
|
|
154
|
+
outputDir: './build/',
|
|
155
|
+
framework: options.framework,
|
|
156
|
+
type: options.type,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Write output
|
|
160
|
+
const outputPath = path.resolve(options.output);
|
|
161
|
+
await ensureDir(path.dirname(outputPath));
|
|
162
|
+
await writeJson(outputPath, taskPlan);
|
|
163
|
+
|
|
164
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
165
|
+
|
|
166
|
+
logger.success(`Task plan created in ${duration}s`);
|
|
167
|
+
logger.json({
|
|
168
|
+
status: 'success',
|
|
169
|
+
totalTasks: compactedTasks.length,
|
|
170
|
+
parallelGroups: parallelGroups.length,
|
|
171
|
+
framework: options.framework,
|
|
172
|
+
type: options.type,
|
|
173
|
+
outputPath,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Print group summary
|
|
177
|
+
for (const group of parallelGroups) {
|
|
178
|
+
logger.info(` Group ${group.group}: ${group.tasks.length} tasks (${group.tasks.map(t => t.id).join(', ')})`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.error(`Plan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function mapFeatureToTaskType(feature: Feature): Task['type'] {
|
|
188
|
+
const name = feature.name.toLowerCase();
|
|
189
|
+
if (name.includes('page') || name.includes('页面')) return 'page';
|
|
190
|
+
if (name.includes('component') || name.includes('组件')) return 'component';
|
|
191
|
+
if (name.includes('api') || name.includes('接口')) return 'api';
|
|
192
|
+
return 'feature';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function estimateHours(feature: Feature): number {
|
|
196
|
+
// Simple estimation based on priority and description length
|
|
197
|
+
const baseHours = feature.priority === 'P0' ? 4 : feature.priority === 'P1' ? 6 : 8;
|
|
198
|
+
const complexityFactor = Math.min(feature.description.length / 100, 3);
|
|
199
|
+
return Math.round(baseHours + complexityFactor);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeTaskText(input: string): string {
|
|
203
|
+
return (input || '')
|
|
204
|
+
.toLowerCase()
|
|
205
|
+
.replace(/[^\u4e00-\u9fa5a-z0-9\s/]/gi, ' ')
|
|
206
|
+
.replace(/\s+/g, ' ')
|
|
207
|
+
.trim();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function deduplicateFeaturesForPlanning(features: Feature[]): Feature[] {
|
|
211
|
+
const seen = new Map<string, Feature>();
|
|
212
|
+
for (const feature of features) {
|
|
213
|
+
const key = normalizeTaskText(feature.name);
|
|
214
|
+
const existing = seen.get(key);
|
|
215
|
+
if (!existing) {
|
|
216
|
+
seen.set(key, feature);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// Keep higher-priority / richer representative.
|
|
220
|
+
const rank = { P0: 0, P1: 1, P2: 2, P3: 3 } as const;
|
|
221
|
+
const existingRank = rank[existing.priority] ?? 3;
|
|
222
|
+
const currentRank = rank[feature.priority] ?? 3;
|
|
223
|
+
if (currentRank < existingRank || feature.description.length > existing.description.length) {
|
|
224
|
+
seen.set(key, feature);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return Array.from(seen.values());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function deduplicatePagesForPlanning(pages: SpecSummary['pages']): SpecSummary['pages'] {
|
|
231
|
+
const seen = new Set<string>();
|
|
232
|
+
return pages.filter(page => {
|
|
233
|
+
const key = normalizeTaskText(page.route || page.name);
|
|
234
|
+
if (seen.has(key)) return false;
|
|
235
|
+
seen.add(key);
|
|
236
|
+
return true;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function deduplicateApisForPlanning(apis: SpecSummary['apis']): SpecSummary['apis'] {
|
|
241
|
+
const seen = new Set<string>();
|
|
242
|
+
return apis.filter(api => {
|
|
243
|
+
const key = normalizeTaskText(`${api.method}:${api.path}`);
|
|
244
|
+
if (seen.has(key)) return false;
|
|
245
|
+
seen.add(key);
|
|
246
|
+
return true;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function deduplicateTasks(tasks: Task[]): Task[] {
|
|
251
|
+
const seen = new Map<string, Task>();
|
|
252
|
+
for (const task of tasks) {
|
|
253
|
+
const key = `${task.type}:${normalizeTaskText(task.name)}`;
|
|
254
|
+
const existing = seen.get(key);
|
|
255
|
+
if (!existing) {
|
|
256
|
+
seen.set(key, { ...task, dependencies: Array.from(new Set(task.dependencies)) });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
existing.dependencies = Array.from(new Set([...existing.dependencies, ...task.dependencies]));
|
|
261
|
+
existing.estimatedHours = Math.max(existing.estimatedHours || 0, task.estimatedHours || 0);
|
|
262
|
+
const priorityRank = { P0: 0, P1: 1, P2: 2, P3: 3 } as const;
|
|
263
|
+
if ((priorityRank[task.priority] ?? 3) < (priorityRank[existing.priority] ?? 3)) {
|
|
264
|
+
existing.priority = task.priority;
|
|
265
|
+
}
|
|
266
|
+
if ((task.description || '').length > (existing.description || '').length) {
|
|
267
|
+
existing.description = task.description;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Re-number IDs to keep compact sequence and remap dependencies.
|
|
272
|
+
const uniqueTasks = Array.from(seen.values());
|
|
273
|
+
const idMap = new Map<string, string>();
|
|
274
|
+
uniqueTasks.forEach((task, idx) => {
|
|
275
|
+
idMap.set(task.id, `T${String(idx + 1).padStart(3, '0')}`);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return uniqueTasks.map((task, idx) => {
|
|
279
|
+
const newId = `T${String(idx + 1).padStart(3, '0')}`;
|
|
280
|
+
const remappedDependencies = Array.from(
|
|
281
|
+
new Set(
|
|
282
|
+
task.dependencies
|
|
283
|
+
.map(dep => idMap.get(dep) || dep)
|
|
284
|
+
.filter(dep => dep !== newId)
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
return {
|
|
288
|
+
...task,
|
|
289
|
+
id: newId,
|
|
290
|
+
dependencies: remappedDependencies,
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function groupTasksByDependencies(tasks: Task[], maxParallel: number): ParallelGroup[] {
|
|
296
|
+
const groups: ParallelGroup[] = [];
|
|
297
|
+
const remaining = new Set(tasks.map(t => t.id));
|
|
298
|
+
const completed = new Set<string>();
|
|
299
|
+
|
|
300
|
+
let groupNum = 1;
|
|
301
|
+
while (remaining.size > 0) {
|
|
302
|
+
// Find tasks whose dependencies are all completed
|
|
303
|
+
const ready = Array.from(remaining).filter(taskId => {
|
|
304
|
+
const task = tasks.find(t => t.id === taskId)!;
|
|
305
|
+
return task.dependencies.every(dep => completed.has(dep));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (ready.length === 0) {
|
|
309
|
+
// Circular dependency or error
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Take up to maxParallel tasks
|
|
314
|
+
const batch = ready.slice(0, maxParallel);
|
|
315
|
+
|
|
316
|
+
groups.push({
|
|
317
|
+
group: groupNum,
|
|
318
|
+
tasks: batch.map(id => tasks.find(t => t.id === id)!),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Mark as completed
|
|
322
|
+
for (const taskId of batch) {
|
|
323
|
+
remaining.delete(taskId);
|
|
324
|
+
completed.add(taskId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
groupNum++;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return groups;
|
|
331
|
+
}
|