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.
Files changed (86) hide show
  1. package/README.md +256 -0
  2. package/bin/spec-agent.js +14 -0
  3. package/dist/commands/analyze.d.ts +16 -0
  4. package/dist/commands/analyze.d.ts.map +1 -0
  5. package/dist/commands/analyze.js +283 -0
  6. package/dist/commands/analyze.js.map +1 -0
  7. package/dist/commands/clean.d.ts +9 -0
  8. package/dist/commands/clean.d.ts.map +1 -0
  9. package/dist/commands/clean.js +109 -0
  10. package/dist/commands/clean.js.map +1 -0
  11. package/dist/commands/dispatch.d.ts +12 -0
  12. package/dist/commands/dispatch.d.ts.map +1 -0
  13. package/dist/commands/dispatch.js +232 -0
  14. package/dist/commands/dispatch.js.map +1 -0
  15. package/dist/commands/doctor.d.ts +9 -0
  16. package/dist/commands/doctor.d.ts.map +1 -0
  17. package/dist/commands/doctor.js +153 -0
  18. package/dist/commands/doctor.js.map +1 -0
  19. package/dist/commands/learn.d.ts +13 -0
  20. package/dist/commands/learn.d.ts.map +1 -0
  21. package/dist/commands/learn.js +234 -0
  22. package/dist/commands/learn.js.map +1 -0
  23. package/dist/commands/merge.d.ts +11 -0
  24. package/dist/commands/merge.d.ts.map +1 -0
  25. package/dist/commands/merge.js +335 -0
  26. package/dist/commands/merge.js.map +1 -0
  27. package/dist/commands/pipeline.d.ts +19 -0
  28. package/dist/commands/pipeline.d.ts.map +1 -0
  29. package/dist/commands/pipeline.js +266 -0
  30. package/dist/commands/pipeline.js.map +1 -0
  31. package/dist/commands/plan.d.ts +13 -0
  32. package/dist/commands/plan.d.ts.map +1 -0
  33. package/dist/commands/plan.js +314 -0
  34. package/dist/commands/plan.js.map +1 -0
  35. package/dist/commands/scan.d.ts +28 -0
  36. package/dist/commands/scan.d.ts.map +1 -0
  37. package/dist/commands/scan.js +488 -0
  38. package/dist/commands/scan.js.map +1 -0
  39. package/dist/commands/status.d.ts +8 -0
  40. package/dist/commands/status.d.ts.map +1 -0
  41. package/dist/commands/status.js +146 -0
  42. package/dist/commands/status.js.map +1 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +126 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/services/document-parser.d.ts +49 -0
  48. package/dist/services/document-parser.d.ts.map +1 -0
  49. package/dist/services/document-parser.js +499 -0
  50. package/dist/services/document-parser.js.map +1 -0
  51. package/dist/services/llm.d.ts +61 -0
  52. package/dist/services/llm.d.ts.map +1 -0
  53. package/dist/services/llm.js +716 -0
  54. package/dist/services/llm.js.map +1 -0
  55. package/dist/types.d.ts +159 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +4 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/utils/file.d.ts +10 -0
  60. package/dist/utils/file.d.ts.map +1 -0
  61. package/dist/utils/file.js +96 -0
  62. package/dist/utils/file.js.map +1 -0
  63. package/dist/utils/logger.d.ts +13 -0
  64. package/dist/utils/logger.d.ts.map +1 -0
  65. package/dist/utils/logger.js +55 -0
  66. package/dist/utils/logger.js.map +1 -0
  67. package/package.json +48 -0
  68. package/scripts/publish-npm.js +174 -0
  69. package/spec-agent-implementation.md +750 -0
  70. package/src/commands/analyze.ts +322 -0
  71. package/src/commands/clean.ts +88 -0
  72. package/src/commands/dispatch.ts +250 -0
  73. package/src/commands/doctor.ts +136 -0
  74. package/src/commands/learn.ts +261 -0
  75. package/src/commands/merge.ts +377 -0
  76. package/src/commands/pipeline.ts +306 -0
  77. package/src/commands/plan.ts +331 -0
  78. package/src/commands/scan.ts +568 -0
  79. package/src/commands/status.ts +129 -0
  80. package/src/index.ts +137 -0
  81. package/src/services/document-parser.ts +548 -0
  82. package/src/services/llm.ts +857 -0
  83. package/src/types.ts +161 -0
  84. package/src/utils/file.ts +60 -0
  85. package/src/utils/logger.ts +58 -0
  86. 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
+ }