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,136 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { Logger } from '../utils/logger';
|
|
4
|
+
import { ensureDir, fileExists } from '../utils/file';
|
|
5
|
+
import { callLLM, getLLMConfig, validateLLMConfig } from '../services/llm';
|
|
6
|
+
|
|
7
|
+
interface DoctorOptions {
|
|
8
|
+
workspace: string;
|
|
9
|
+
checkLlm?: boolean;
|
|
10
|
+
format: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CheckResult {
|
|
14
|
+
name: string;
|
|
15
|
+
status: 'pass' | 'warn' | 'fail';
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseNodeMajorVersion(version: string): number {
|
|
20
|
+
const raw = version.startsWith('v') ? version.slice(1) : version;
|
|
21
|
+
const major = parseInt(raw.split('.')[0], 10);
|
|
22
|
+
return Number.isFinite(major) ? major : 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function doctorCommand(options: DoctorOptions, command: Command): Promise<void> {
|
|
26
|
+
const logger = new Logger();
|
|
27
|
+
const checks: CheckResult[] = [];
|
|
28
|
+
const workspacePath = path.resolve(options.workspace);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const nodeMajor = parseNodeMajorVersion(process.version);
|
|
32
|
+
checks.push({
|
|
33
|
+
name: 'node_version',
|
|
34
|
+
status: nodeMajor >= 18 ? 'pass' : 'fail',
|
|
35
|
+
message: `当前 Node 版本: ${process.version}(要求 >= 18)`,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await ensureDir(workspacePath);
|
|
40
|
+
checks.push({
|
|
41
|
+
name: 'workspace_access',
|
|
42
|
+
status: 'pass',
|
|
43
|
+
message: `工作目录可访问: ${workspacePath}`,
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
checks.push({
|
|
47
|
+
name: 'workspace_access',
|
|
48
|
+
status: 'fail',
|
|
49
|
+
message: `工作目录不可写: ${error instanceof Error ? error.message : String(error)}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const llmConfig = getLLMConfig();
|
|
54
|
+
try {
|
|
55
|
+
validateLLMConfig(llmConfig);
|
|
56
|
+
checks.push({
|
|
57
|
+
name: 'llm_config',
|
|
58
|
+
status: 'pass',
|
|
59
|
+
message: `LLM 配置有效: ${llmConfig.model} @ ${llmConfig.baseUrl}`,
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
checks.push({
|
|
63
|
+
name: 'llm_config',
|
|
64
|
+
status: 'fail',
|
|
65
|
+
message: error instanceof Error ? error.message : String(error),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const patternsPath = path.join(workspacePath, '.patterns.json');
|
|
70
|
+
const hasPatterns = await fileExists(patternsPath);
|
|
71
|
+
checks.push({
|
|
72
|
+
name: 'learned_patterns',
|
|
73
|
+
status: hasPatterns ? 'pass' : 'warn',
|
|
74
|
+
message: hasPatterns
|
|
75
|
+
? `检测到模式文件: ${patternsPath}`
|
|
76
|
+
: '未检测到 .patterns.json(首次运行可忽略)',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (options.checkLlm) {
|
|
80
|
+
try {
|
|
81
|
+
validateLLMConfig(llmConfig);
|
|
82
|
+
const probeConfig = {
|
|
83
|
+
...llmConfig,
|
|
84
|
+
maxTokens: 32,
|
|
85
|
+
temperature: 0,
|
|
86
|
+
};
|
|
87
|
+
await callLLM(
|
|
88
|
+
'请返回 JSON:{"ok": true, "purpose": "healthcheck"}',
|
|
89
|
+
probeConfig,
|
|
90
|
+
logger
|
|
91
|
+
);
|
|
92
|
+
checks.push({
|
|
93
|
+
name: 'llm_connectivity',
|
|
94
|
+
status: 'pass',
|
|
95
|
+
message: 'LLM 连通性检查通过',
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
checks.push({
|
|
99
|
+
name: 'llm_connectivity',
|
|
100
|
+
status: 'fail',
|
|
101
|
+
message: `LLM 连通性检查失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const failCount = checks.filter(c => c.status === 'fail').length;
|
|
107
|
+
const warnCount = checks.filter(c => c.status === 'warn').length;
|
|
108
|
+
|
|
109
|
+
if (options.format === 'json') {
|
|
110
|
+
logger.json({
|
|
111
|
+
status: failCount > 0 ? 'fail' : warnCount > 0 ? 'warn' : 'pass',
|
|
112
|
+
checks,
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
logger.info(`Doctor workspace: ${workspacePath}`);
|
|
116
|
+
for (const check of checks) {
|
|
117
|
+
const prefix = check.status === 'pass' ? 'PASS' : check.status === 'warn' ? 'WARN' : 'FAIL';
|
|
118
|
+
logger.info(` [${prefix}] ${check.name}: ${check.message}`);
|
|
119
|
+
}
|
|
120
|
+
if (failCount === 0 && warnCount === 0) {
|
|
121
|
+
logger.success('所有检查通过');
|
|
122
|
+
} else if (failCount === 0) {
|
|
123
|
+
logger.warn(`检查完成:${warnCount} 警告`);
|
|
124
|
+
} else {
|
|
125
|
+
logger.error(`检查失败:${failCount} 错误, ${warnCount} 警告`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (failCount > 0) {
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
logger.error(`Doctor failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
findFiles
|
|
10
|
+
} from '../utils/file';
|
|
11
|
+
import { ChunkSummary, Patterns, LearnedPattern } from '../types';
|
|
12
|
+
|
|
13
|
+
interface LearnOptions {
|
|
14
|
+
workspace: string;
|
|
15
|
+
from?: string;
|
|
16
|
+
pattern?: string;
|
|
17
|
+
rule?: string;
|
|
18
|
+
list?: boolean;
|
|
19
|
+
export?: string;
|
|
20
|
+
apply?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PATTERNS_FILE = '.patterns.json';
|
|
24
|
+
|
|
25
|
+
export async function learnCommand(options: LearnOptions, command: Command): Promise<void> {
|
|
26
|
+
const logger = new Logger();
|
|
27
|
+
const workspacePath = path.resolve(options.workspace);
|
|
28
|
+
const patternsPath = path.join(workspacePath, PATTERNS_FILE);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Load existing patterns
|
|
32
|
+
let patterns: Patterns = {
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
workspace: workspacePath,
|
|
35
|
+
patterns: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (await fileExists(patternsPath)) {
|
|
39
|
+
patterns = await readJson<Patterns>(patternsPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Handle list command
|
|
43
|
+
if (options.list) {
|
|
44
|
+
if (patterns.patterns.length === 0) {
|
|
45
|
+
logger.info('No learned patterns yet.');
|
|
46
|
+
} else {
|
|
47
|
+
logger.info(`Learned patterns (${patterns.patterns.length}):`);
|
|
48
|
+
for (const p of patterns.patterns) {
|
|
49
|
+
logger.info(` - ${p.name}: ${p.rule} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle export command
|
|
56
|
+
if (options.export) {
|
|
57
|
+
const exportPath = path.resolve(options.export);
|
|
58
|
+
await writeJson(exportPath, patterns);
|
|
59
|
+
logger.success(`Patterns exported to ${exportPath}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle manual pattern learning
|
|
64
|
+
if (options.pattern && options.rule) {
|
|
65
|
+
const newPattern: LearnedPattern = {
|
|
66
|
+
name: options.pattern,
|
|
67
|
+
rule: options.rule,
|
|
68
|
+
context: 'manual',
|
|
69
|
+
confidence: 1.0,
|
|
70
|
+
learnedAt: new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Check if pattern already exists
|
|
74
|
+
const existingIndex = patterns.patterns.findIndex(p => p.name === options.pattern);
|
|
75
|
+
if (existingIndex >= 0) {
|
|
76
|
+
patterns.patterns[existingIndex] = newPattern;
|
|
77
|
+
logger.info(`Updated pattern: ${options.pattern}`);
|
|
78
|
+
} else {
|
|
79
|
+
patterns.patterns.push(newPattern);
|
|
80
|
+
logger.info(`Learned new pattern: ${options.pattern}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await writeJson(patternsPath, patterns);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle automatic learning from phase results
|
|
88
|
+
if (options.from) {
|
|
89
|
+
const learnedCount = await learnFromPhase(workspacePath, options.from, patterns, logger);
|
|
90
|
+
|
|
91
|
+
if (learnedCount > 0) {
|
|
92
|
+
await writeJson(patternsPath, patterns);
|
|
93
|
+
logger.success(`Learned ${learnedCount} new patterns from ${options.from}`);
|
|
94
|
+
} else {
|
|
95
|
+
logger.info('No new patterns learned.');
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle apply command
|
|
101
|
+
if (options.apply) {
|
|
102
|
+
if (patterns.patterns.length === 0) {
|
|
103
|
+
logger.warn('No patterns to apply.');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.info(`Applied ${patterns.patterns.length} learned patterns to workspace`);
|
|
108
|
+
logger.json({
|
|
109
|
+
status: 'success',
|
|
110
|
+
patternsApplied: patterns.patterns.length,
|
|
111
|
+
patterns: patterns.patterns.map(p => p.name),
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default: show help
|
|
117
|
+
logger.info('Usage:');
|
|
118
|
+
logger.info(' spec-agent learn --workspace ./workspace --from summaries');
|
|
119
|
+
logger.info(' spec-agent learn --workspace ./workspace --pattern "API规范" --rule "/api/v{version}/{resource}"');
|
|
120
|
+
logger.info(' spec-agent learn --workspace ./workspace --list');
|
|
121
|
+
logger.info(' spec-agent learn --workspace ./workspace --export ./patterns.json');
|
|
122
|
+
|
|
123
|
+
} catch (error) {
|
|
124
|
+
logger.error(`Learn failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function learnFromPhase(
|
|
130
|
+
workspacePath: string,
|
|
131
|
+
phase: string,
|
|
132
|
+
patterns: Patterns,
|
|
133
|
+
logger: Logger
|
|
134
|
+
): Promise<number> {
|
|
135
|
+
let learnedCount = 0;
|
|
136
|
+
|
|
137
|
+
switch (phase) {
|
|
138
|
+
case 'summaries':
|
|
139
|
+
learnedCount = await learnFromSummaries(workspacePath, patterns, logger);
|
|
140
|
+
break;
|
|
141
|
+
case 'plan':
|
|
142
|
+
learnedCount = await learnFromPlan(workspacePath, patterns, logger);
|
|
143
|
+
break;
|
|
144
|
+
case 'dispatch':
|
|
145
|
+
learnedCount = await learnFromDispatch(workspacePath, patterns, logger);
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
logger.error(`Unknown phase: ${phase}`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return learnedCount;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function learnFromSummaries(
|
|
156
|
+
workspacePath: string,
|
|
157
|
+
patterns: Patterns,
|
|
158
|
+
logger: Logger
|
|
159
|
+
): Promise<number> {
|
|
160
|
+
const summariesDir = path.join(workspacePath, 'summaries');
|
|
161
|
+
|
|
162
|
+
if (!(await fileExists(summariesDir))) {
|
|
163
|
+
logger.error('No summaries directory found. Run analyze first.');
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const summaryFiles = await findFiles('chunk_*_summary.json', summariesDir);
|
|
168
|
+
if (summaryFiles.length === 0) {
|
|
169
|
+
logger.error('No summary files found.');
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let learnedCount = 0;
|
|
174
|
+
|
|
175
|
+
for (const file of summaryFiles) {
|
|
176
|
+
const summary = await readJson<ChunkSummary>(file);
|
|
177
|
+
|
|
178
|
+
// Learn naming patterns from features
|
|
179
|
+
for (const feature of summary.features) {
|
|
180
|
+
const namingPattern = extractNamingPattern(feature.name);
|
|
181
|
+
if (namingPattern && !patternExists(patterns, namingPattern.name)) {
|
|
182
|
+
patterns.patterns.push({
|
|
183
|
+
...namingPattern,
|
|
184
|
+
context: `feature:${feature.id}`,
|
|
185
|
+
confidence: 0.8,
|
|
186
|
+
learnedAt: new Date().toISOString(),
|
|
187
|
+
});
|
|
188
|
+
learnedCount++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Learn API patterns
|
|
193
|
+
for (const api of summary.apis) {
|
|
194
|
+
const apiPattern = extractApiPattern(api.path);
|
|
195
|
+
if (apiPattern && !patternExists(patterns, apiPattern.name)) {
|
|
196
|
+
patterns.patterns.push({
|
|
197
|
+
...apiPattern,
|
|
198
|
+
context: `api:${api.method}`,
|
|
199
|
+
confidence: 0.9,
|
|
200
|
+
learnedAt: new Date().toISOString(),
|
|
201
|
+
});
|
|
202
|
+
learnedCount++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return learnedCount;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function learnFromPlan(
|
|
211
|
+
workspacePath: string,
|
|
212
|
+
patterns: Patterns,
|
|
213
|
+
logger: Logger
|
|
214
|
+
): Promise<number> {
|
|
215
|
+
// Learn from task planning patterns
|
|
216
|
+
// This would analyze task_plan.json to learn estimation patterns, etc.
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function learnFromDispatch(
|
|
221
|
+
workspacePath: string,
|
|
222
|
+
patterns: Patterns,
|
|
223
|
+
logger: Logger
|
|
224
|
+
): Promise<number> {
|
|
225
|
+
// Learn from dispatch patterns
|
|
226
|
+
// This would analyze dispatch_plan.json to learn agent assignment patterns
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function extractNamingPattern(name: string): { name: string; rule: string } | null {
|
|
231
|
+
// Detect naming conventions
|
|
232
|
+
if (/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
|
|
233
|
+
return { name: '组件命名规范', rule: 'PascalCase' };
|
|
234
|
+
}
|
|
235
|
+
if (/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
236
|
+
return { name: '路由命名规范', rule: 'kebab-case' };
|
|
237
|
+
}
|
|
238
|
+
if (/^[a-z][a-zA-Z0-9]*$/.test(name)) {
|
|
239
|
+
return { name: '变量命名规范', rule: 'camelCase' };
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function extractApiPattern(path: string): { name: string; rule: string } | null {
|
|
245
|
+
// Detect API patterns
|
|
246
|
+
const versionMatch = path.match(/\/api\/v(\d+)\//);
|
|
247
|
+
if (versionMatch) {
|
|
248
|
+
return { name: 'API版本规范', rule: `/api/v{version}/...` };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const restMatch = path.match(/\/api\/\w+\/(\d+|\w+)/);
|
|
252
|
+
if (restMatch) {
|
|
253
|
+
return { name: 'RESTful路径规范', rule: `/api/{resource}/{id}` };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function patternExists(patterns: Patterns, name: string): boolean {
|
|
260
|
+
return patterns.patterns.some(p => p.name === name);
|
|
261
|
+
}
|