speccore 8.3.349 → 8.3.363
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/.agents/skills/spec-verify/SKILL.md +22 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/about.d.ts.map +1 -1
- package/dist/commands/about.js +36 -1
- package/dist/commands/about.js.map +1 -1
- package/dist/commands/analyze/add-platform.d.ts +3 -0
- package/dist/commands/analyze/add-platform.d.ts.map +1 -0
- package/dist/commands/analyze/add-platform.js +82 -0
- package/dist/commands/analyze/add-platform.js.map +1 -0
- package/dist/commands/analyze/audit-fix.d.ts +3 -0
- package/dist/commands/analyze/audit-fix.d.ts.map +1 -0
- package/dist/commands/analyze/audit-fix.js +73 -0
- package/dist/commands/analyze/audit-fix.js.map +1 -0
- package/dist/commands/analyze/context-guard.d.ts +3 -0
- package/dist/commands/analyze/context-guard.d.ts.map +1 -0
- package/dist/commands/analyze/context-guard.js +75 -0
- package/dist/commands/analyze/context-guard.js.map +1 -0
- package/dist/commands/analyze/doc.d.ts +3 -0
- package/dist/commands/analyze/doc.d.ts.map +1 -0
- package/dist/commands/analyze/doc.js +77 -0
- package/dist/commands/analyze/doc.js.map +1 -0
- package/dist/commands/analyze/feature.d.ts +3 -0
- package/dist/commands/analyze/feature.d.ts.map +1 -0
- package/dist/commands/analyze/feature.js +75 -0
- package/dist/commands/analyze/feature.js.map +1 -0
- package/dist/commands/analyze/from-code.d.ts +3 -0
- package/dist/commands/analyze/from-code.d.ts.map +1 -0
- package/dist/commands/analyze/from-code.js +94 -0
- package/dist/commands/analyze/from-code.js.map +1 -0
- package/dist/commands/analyze/incremental.d.ts +3 -0
- package/dist/commands/analyze/incremental.d.ts.map +1 -0
- package/dist/commands/analyze/incremental.js +90 -0
- package/dist/commands/analyze/incremental.js.map +1 -0
- package/dist/commands/analyze/modes.d.ts +13 -0
- package/dist/commands/analyze/modes.d.ts.map +1 -0
- package/dist/commands/analyze/modes.js +147 -0
- package/dist/commands/analyze/modes.js.map +1 -0
- package/dist/commands/analyze/module.d.ts +3 -0
- package/dist/commands/analyze/module.d.ts.map +1 -0
- package/dist/commands/analyze/module.js +95 -0
- package/dist/commands/analyze/module.js.map +1 -0
- package/dist/commands/analyze/overview.d.ts +3 -0
- package/dist/commands/analyze/overview.d.ts.map +1 -0
- package/dist/commands/analyze/overview.js +125 -0
- package/dist/commands/analyze/overview.js.map +1 -0
- package/dist/commands/analyze/shared.d.ts +53 -0
- package/dist/commands/analyze/shared.d.ts.map +1 -0
- package/dist/commands/analyze/shared.js +34 -0
- package/dist/commands/analyze/shared.js.map +1 -0
- package/dist/commands/analyze/units.d.ts +46 -0
- package/dist/commands/analyze/units.d.ts.map +1 -0
- package/dist/commands/analyze/units.js +950 -0
- package/dist/commands/analyze/units.js.map +1 -0
- package/dist/commands/analyze.d.ts +6 -88
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +97 -1417
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/ask.d.ts.map +1 -1
- package/dist/commands/ask.js +4 -2
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +36 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +153 -0
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/doc2spec.d.ts +1 -1
- package/dist/commands/doc2spec.d.ts.map +1 -1
- package/dist/commands/doc2spec.js +33 -23
- package/dist/commands/doc2spec.js.map +1 -1
- package/dist/commands/doctor.js +60 -13
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +22 -0
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/help.js +36 -1
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pipeline.d.ts.map +1 -1
- package/dist/commands/pipeline.js +15 -0
- package/dist/commands/pipeline.js.map +1 -1
- package/dist/commands/plan.js +2 -0
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/pr.d.ts.map +1 -1
- package/dist/commands/pr.js +14 -3
- package/dist/commands/pr.js.map +1 -1
- package/dist/commands/prompts.d.ts +11 -1
- package/dist/commands/prompts.d.ts.map +1 -1
- package/dist/commands/prompts.js +197 -15
- package/dist/commands/prompts.js.map +1 -1
- package/dist/commands/reindex.d.ts.map +1 -1
- package/dist/commands/reindex.js +23 -0
- package/dist/commands/reindex.js.map +1 -1
- package/dist/commands/retro.d.ts.map +1 -1
- package/dist/commands/retro.js +3 -1
- package/dist/commands/retro.js.map +1 -1
- package/dist/commands/status-panel.d.ts.map +1 -1
- package/dist/commands/status-panel.js +9 -1
- package/dist/commands/status-panel.js.map +1 -1
- package/dist/commands/unitize.d.ts +2 -0
- package/dist/commands/unitize.d.ts.map +1 -1
- package/dist/commands/unitize.js +168 -6
- package/dist/commands/unitize.js.map +1 -1
- package/dist/commands/update-env-configs.d.ts.map +1 -1
- package/dist/commands/update-env-configs.js +10 -0
- package/dist/commands/update-env-configs.js.map +1 -1
- package/dist/commands/verify.d.ts +8 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +383 -16
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/welcome.d.ts.map +1 -1
- package/dist/commands/welcome.js +3 -1
- package/dist/commands/welcome.js.map +1 -1
- package/dist/core/ai-verify/plan-generator.d.ts +94 -0
- package/dist/core/ai-verify/plan-generator.d.ts.map +1 -0
- package/dist/core/ai-verify/plan-generator.js +598 -0
- package/dist/core/ai-verify/plan-generator.js.map +1 -0
- package/dist/core/ai-verify/result-parser.d.ts +27 -0
- package/dist/core/ai-verify/result-parser.d.ts.map +1 -0
- package/dist/core/ai-verify/result-parser.js +212 -0
- package/dist/core/ai-verify/result-parser.js.map +1 -0
- package/dist/core/ai-verify/scope-resolver.d.ts +34 -0
- package/dist/core/ai-verify/scope-resolver.d.ts.map +1 -0
- package/dist/core/ai-verify/scope-resolver.js +183 -0
- package/dist/core/ai-verify/scope-resolver.js.map +1 -0
- package/dist/core/ai-verify/types.d.ts +171 -0
- package/dist/core/ai-verify/types.d.ts.map +1 -0
- package/dist/core/ai-verify/types.js +9 -0
- package/dist/core/ai-verify/types.js.map +1 -0
- package/dist/core/ai-verify/visual-judge.d.ts +33 -0
- package/dist/core/ai-verify/visual-judge.d.ts.map +1 -0
- package/dist/core/ai-verify/visual-judge.js +123 -0
- package/dist/core/ai-verify/visual-judge.js.map +1 -0
- package/dist/core/ask-engine.d.ts +6 -0
- package/dist/core/ask-engine.d.ts.map +1 -1
- package/dist/core/ask-engine.js +83 -3
- package/dist/core/ask-engine.js.map +1 -1
- package/dist/core/code-graph/builder.d.ts.map +1 -1
- package/dist/core/code-graph/builder.js +93 -75
- package/dist/core/code-graph/builder.js.map +1 -1
- package/dist/core/code-graph/incremental-build.d.ts.map +1 -1
- package/dist/core/code-graph/incremental-build.js +13 -19
- package/dist/core/code-graph/incremental-build.js.map +1 -1
- package/dist/core/code-graph/index.d.ts +1 -1
- package/dist/core/code-graph/index.d.ts.map +1 -1
- package/dist/core/code-graph/index.js +15 -12
- package/dist/core/code-graph/index.js.map +1 -1
- package/dist/core/code-graph/multimodal.js +1 -1
- package/dist/core/code-graph/parser.js +1 -1
- package/dist/core/code-graph/query.d.ts +1 -1
- package/dist/core/code-graph/reporter.d.ts +1 -1
- package/dist/core/code-graph/reporter.d.ts.map +1 -1
- package/dist/core/code-graph/reporter.js +18 -10
- package/dist/core/code-graph/reporter.js.map +1 -1
- package/dist/core/code-graph/requirement-linker.d.ts.map +1 -1
- package/dist/core/code-graph/requirement-linker.js +24 -14
- package/dist/core/code-graph/requirement-linker.js.map +1 -1
- package/dist/core/code-graph/types.d.ts +1 -1
- package/dist/core/code-graph/types.js +1 -1
- package/dist/core/code-graph/visualizer.d.ts +1 -7
- package/dist/core/code-graph/visualizer.d.ts.map +1 -1
- package/dist/core/code-graph/visualizer.js +476 -49
- package/dist/core/code-graph/visualizer.js.map +1 -1
- package/dist/core/deploy/engine.d.ts +21 -0
- package/dist/core/deploy/engine.d.ts.map +1 -1
- package/dist/core/deploy/engine.js +276 -0
- package/dist/core/deploy/engine.js.map +1 -1
- package/dist/core/deploy-ledger.d.ts +12 -0
- package/dist/core/deploy-ledger.d.ts.map +1 -0
- package/dist/core/deploy-ledger.js +35 -0
- package/dist/core/deploy-ledger.js.map +1 -0
- package/dist/core/environment-config.d.ts +11 -0
- package/dist/core/environment-config.d.ts.map +1 -1
- package/dist/core/environment-config.js +4 -0
- package/dist/core/environment-config.js.map +1 -1
- package/dist/core/intent-cache.d.ts.map +1 -1
- package/dist/core/intent-cache.js +6 -0
- package/dist/core/intent-cache.js.map +1 -1
- package/dist/core/knowledge-visualizer.js +1 -1
- package/dist/core/page-bundle.d.ts +2 -0
- package/dist/core/page-bundle.d.ts.map +1 -1
- package/dist/core/page-bundle.js +18 -1
- package/dist/core/page-bundle.js.map +1 -1
- package/dist/core/plan-html.js +1 -1
- package/dist/core/platform-inference.d.ts +35 -0
- package/dist/core/platform-inference.d.ts.map +1 -1
- package/dist/core/platform-inference.js +92 -0
- package/dist/core/platform-inference.js.map +1 -1
- package/dist/core/prompt-plugins/analyze-graph-plugin.d.ts.map +1 -1
- package/dist/core/prompt-plugins/analyze-graph-plugin.js +16 -1
- package/dist/core/prompt-plugins/analyze-graph-plugin.js.map +1 -1
- package/dist/core/prompt-plugins/execute-graph-plugin.d.ts +2 -0
- package/dist/core/prompt-plugins/execute-graph-plugin.d.ts.map +1 -1
- package/dist/core/prompt-plugins/execute-graph-plugin.js +127 -27
- package/dist/core/prompt-plugins/execute-graph-plugin.js.map +1 -1
- package/dist/core/quality-audit.d.ts.map +1 -1
- package/dist/core/quality-audit.js +18 -0
- package/dist/core/quality-audit.js.map +1 -1
- package/dist/core/reindex-engine.d.ts +20 -0
- package/dist/core/reindex-engine.d.ts.map +1 -1
- package/dist/core/reindex-engine.js +142 -0
- package/dist/core/reindex-engine.js.map +1 -1
- package/dist/core/requirement-clarifier.d.ts.map +1 -1
- package/dist/core/requirement-clarifier.js +22 -6
- package/dist/core/requirement-clarifier.js.map +1 -1
- package/dist/core/spec-paths.d.ts +5 -0
- package/dist/core/spec-paths.d.ts.map +1 -1
- package/dist/core/spec-paths.js +1 -0
- package/dist/core/spec-paths.js.map +1 -1
- package/dist/core/test-config.d.ts +7 -1
- package/dist/core/test-config.d.ts.map +1 -1
- package/dist/core/test-config.js +59 -11
- package/dist/core/test-config.js.map +1 -1
- package/dist/core/ui-verify/index.d.ts +1 -1
- package/dist/core/ui-verify/index.d.ts.map +1 -1
- package/dist/core/ui-verify/index.js +2 -1
- package/dist/core/ui-verify/index.js.map +1 -1
- package/dist/core/ui-verify/smoke-engine.js +71 -15
- package/dist/core/ui-verify/smoke-engine.js.map +1 -1
- package/dist/core/ui-verify/spec-generator.d.ts +5 -9
- package/dist/core/ui-verify/spec-generator.d.ts.map +1 -1
- package/dist/core/ui-verify/spec-generator.js +66 -3
- package/dist/core/ui-verify/spec-generator.js.map +1 -1
- package/dist/core/ui-verify/types.d.ts +25 -0
- package/dist/core/ui-verify/types.d.ts.map +1 -1
- package/dist/core/ui-verify/visual-model-client.d.ts +2 -0
- package/dist/core/ui-verify/visual-model-client.d.ts.map +1 -1
- package/dist/core/ui-verify/visual-model-client.js +4 -4
- package/dist/core/ui-verify/visual-model-client.js.map +1 -1
- package/dist/core/unified-config.d.ts +27 -0
- package/dist/core/unified-config.d.ts.map +1 -1
- package/dist/core/unified-config.js +37 -0
- package/dist/core/unified-config.js.map +1 -1
- package/package.json +1 -1
- package/templates/accounts-example.yaml +49 -0
- package/templates/html/fonts/JetBrainsMono.woff2 +0 -0
- package/templates/html/fonts/Orbitron.woff2 +0 -0
- package/templates/html/speccore-ask-explain.html +1 -1
- package/templates/html/speccore-ask-guide.html +2 -2
- package/templates/html/speccore-ask-match.html +1 -1
- package/templates/html/speccore-ask-pipeline.html +1 -1
- package/templates/html/speccore-ask-result.html +2 -2
- package/templates/html/speccore-help.html +2 -2
- package/templates/html/speccore-setup-guide.html +16 -1
- package/templates/speccore-yml-example.yml +12 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.UNITS_CACHE_DIR = void 0;
|
|
37
|
+
exports.extractIterationUnits = extractIterationUnits;
|
|
38
|
+
exports.saveUnitAnalysis = saveUnitAnalysis;
|
|
39
|
+
exports.loadUnitAnalyses = loadUnitAnalyses;
|
|
40
|
+
exports.detectPendingUnits = detectPendingUnits;
|
|
41
|
+
exports.consolidateUnitAnalyses = consolidateUnitAnalyses;
|
|
42
|
+
exports.buildUnitAnalysisPrompt = buildUnitAnalysisPrompt;
|
|
43
|
+
exports.buildAndSaveQualityReport = buildAndSaveQualityReport;
|
|
44
|
+
exports.analyzeUnitModes = analyzeUnitModes;
|
|
45
|
+
/**
|
|
46
|
+
* analyze 模式:功能单元聚焦分析(--extract-units / --unit / --consolidate / --resume-units / --auto-units)
|
|
47
|
+
* 含单元清单 CRUD、单元分析 Prompt、调度循环与质量报告——单元域全家桶。
|
|
48
|
+
* v8.3.359+: 从 analyze.ts 拆分(渐进重构批次 D,逻辑不搬家、行为不变)
|
|
49
|
+
*/
|
|
50
|
+
const path_1 = require("path");
|
|
51
|
+
const fs_extra_1 = require("fs-extra");
|
|
52
|
+
const logger_1 = require("../../utils/logger");
|
|
53
|
+
const context_1 = require("../../core/context");
|
|
54
|
+
const feature_source_1 = require("../../core/feature-source");
|
|
55
|
+
const unit_context_assembler_1 = require("../../core/unit-context-assembler");
|
|
56
|
+
const spec_paths_1 = require("../../core/spec-paths");
|
|
57
|
+
const spec_skeleton_1 = require("../../core/spec-skeleton");
|
|
58
|
+
const dispatch_loop_1 = require("../../core/dispatch-loop");
|
|
59
|
+
const agent_adapter_1 = require("../../core/agent-adapter");
|
|
60
|
+
const task_utils_1 = require("../../utils/task-utils");
|
|
61
|
+
const ai_trace_1 = require("../../utils/ai-trace");
|
|
62
|
+
const shared_1 = require("./shared");
|
|
63
|
+
exports.UNITS_CACHE_DIR = '.speccore/cache/unit-analysis';
|
|
64
|
+
/** 从需求文档提取功能单元清单 */
|
|
65
|
+
async function extractIterationUnits(iterDir) {
|
|
66
|
+
const reqDir = (0, path_1.join)(iterDir, '010-requirements');
|
|
67
|
+
const units = [];
|
|
68
|
+
let idCounter = 1;
|
|
69
|
+
if (!await (0, fs_extra_1.pathExists)(reqDir))
|
|
70
|
+
return units;
|
|
71
|
+
// v8.3.201+: 优先读 010-requirements/features/ 的一级子目录名作为功能单元(与 doc2spec 切分目录对齐)。
|
|
72
|
+
// 旧逻辑「递归扫描所有 .md 的 ## 标题」会误读 _matrix.md / GLOBAL_CONTEXT-*.md / CHANGELOG.md,
|
|
73
|
+
// 反而读不到真正的功能单元目录名。
|
|
74
|
+
const featuresDir = (0, path_1.join)(reqDir, 'features');
|
|
75
|
+
if (await (0, fs_extra_1.pathExists)(featuresDir)) {
|
|
76
|
+
const entries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
|
|
77
|
+
// v8.3.267: 过滤 overview- 前缀的全局章节目录(unitize 误切产物,
|
|
78
|
+
// 如「核心业务规则跨端通用」),防止假单元混入调度循环
|
|
79
|
+
const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('_') && !e.name.startsWith('overview-'));
|
|
80
|
+
for (const dir of dirs) {
|
|
81
|
+
const readmePath = await (0, feature_source_1.resolveUnitSourcePath)((0, path_1.join)(featuresDir, dir.name));
|
|
82
|
+
let content = '';
|
|
83
|
+
if (await (0, fs_extra_1.pathExists)(readmePath)) {
|
|
84
|
+
// v8.3.267: 图片引用可能落在 500 字符截断之外,显式保留引用行,
|
|
85
|
+
// 保证单元上下文的图片清单完整(AI 可据此 Read 原型图)
|
|
86
|
+
const full = await (0, fs_extra_1.readFile)(readmePath, 'utf-8');
|
|
87
|
+
content = full.slice(0, 500);
|
|
88
|
+
const imgRefs = new Set();
|
|
89
|
+
const imgRe = /!\[[^\]]*\]\(([^)\s]+)\)/g;
|
|
90
|
+
let m;
|
|
91
|
+
while ((m = imgRe.exec(full)) !== null)
|
|
92
|
+
imgRefs.add(m[1]);
|
|
93
|
+
const outside = [...imgRefs].filter(r => !content.includes(r));
|
|
94
|
+
if (outside.length > 0) {
|
|
95
|
+
content += `\n\n<!-- 原文图片引用(截断外保留)-->\n${outside.map(r => ``).join('\n')}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
units.push({
|
|
99
|
+
id: `M-${String(idCounter++).padStart(2, '0')}`,
|
|
100
|
+
name: dir.name,
|
|
101
|
+
source: (0, path_1.relative)(reqDir, readmePath).replace(/\\/g, '/'),
|
|
102
|
+
sourceRange: { startLine: 1, endLine: 1 },
|
|
103
|
+
content,
|
|
104
|
+
});
|
|
105
|
+
if (idCounter > 30)
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (units.length > 0)
|
|
109
|
+
return units;
|
|
110
|
+
}
|
|
111
|
+
const reqFiles = [];
|
|
112
|
+
// v8.3.210+: 系统报告/验证文档(非功能单元),兜底扫描时跳过
|
|
113
|
+
const systemDocs = new Set(['VALIDATION', 'VALIDATION_REPORT', 'VERIFICATION', 'QUALITY_REPORT', 'CLARIFY_REPORT', 'INDEX', 'README', 'CHANGELOG', 'FUNCTION_MAP', 'GLOSSARY']);
|
|
114
|
+
const resourceDirs = new Set(['assets', 'images', 'prototypes', 'screenshots', 'sources', 'designs', 'extracted']);
|
|
115
|
+
async function scanDir(dir) {
|
|
116
|
+
const entries = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const fullPath = (0, path_1.join)(dir, entry.name);
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
if (resourceDirs.has(entry.name))
|
|
121
|
+
continue; // 不递归资源目录
|
|
122
|
+
await scanDir(fullPath);
|
|
123
|
+
}
|
|
124
|
+
else if (entry.name.endsWith('.md')) {
|
|
125
|
+
const base = entry.name.slice(0, -3);
|
|
126
|
+
if (systemDocs.has(base) || base.startsWith('unit-') || base.startsWith('_'))
|
|
127
|
+
continue;
|
|
128
|
+
reqFiles.push(fullPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
await scanDir(reqDir);
|
|
133
|
+
for (const file of reqFiles) {
|
|
134
|
+
const content = await (0, fs_extra_1.readFile)(file, 'utf-8');
|
|
135
|
+
const lines = content.split(/\r?\n/);
|
|
136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
137
|
+
const line = lines[i];
|
|
138
|
+
const headingMatch = line.match(/^#{2,3}\s+(.+)$/);
|
|
139
|
+
if (!headingMatch)
|
|
140
|
+
continue;
|
|
141
|
+
const name = headingMatch[1].trim();
|
|
142
|
+
// 跳过通用标题
|
|
143
|
+
const skipKeywords = ['需求概述', '术语表', '附录', '测试策略', '参考资料', '目录', '引言', '背景'];
|
|
144
|
+
if (skipKeywords.some(k => name.includes(k)))
|
|
145
|
+
continue;
|
|
146
|
+
// 收集后续 1-15 行作为内容摘要
|
|
147
|
+
let descLines = [];
|
|
148
|
+
for (let j = i + 1; j < Math.min(i + 16, lines.length); j++) {
|
|
149
|
+
const l = lines[j];
|
|
150
|
+
if (l.match(/^#{1,3}\s+/))
|
|
151
|
+
break;
|
|
152
|
+
if (l.trim())
|
|
153
|
+
descLines.push(l.trim());
|
|
154
|
+
}
|
|
155
|
+
units.push({
|
|
156
|
+
id: `M-${String(idCounter++).padStart(2, '0')}`,
|
|
157
|
+
name,
|
|
158
|
+
source: (0, path_1.relative)(reqDir, file),
|
|
159
|
+
sourceRange: { startLine: i + 1, endLine: i + 1 + descLines.length },
|
|
160
|
+
content: descLines.join('\n').slice(0, 500),
|
|
161
|
+
});
|
|
162
|
+
if (idCounter > 30)
|
|
163
|
+
break; // 最多识别 30 个功能单元
|
|
164
|
+
}
|
|
165
|
+
if (idCounter > 30)
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
return units;
|
|
169
|
+
}
|
|
170
|
+
/** 保存单元分析结果到缓存 */
|
|
171
|
+
async function saveUnitAnalysis(iteration, analysis) {
|
|
172
|
+
const cacheDir = (0, path_1.join)(((0, task_utils_1.findProjectRoot)() || process.cwd()), exports.UNITS_CACHE_DIR, iteration);
|
|
173
|
+
await (0, fs_extra_1.ensureDir)(cacheDir);
|
|
174
|
+
const fp = (0, path_1.join)(cacheDir, `${analysis.id}.json`);
|
|
175
|
+
await (0, fs_extra_1.writeFile)(fp, JSON.stringify(analysis, null, 2));
|
|
176
|
+
}
|
|
177
|
+
/** 读取已完成的单元分析 */
|
|
178
|
+
async function loadUnitAnalyses(iteration) {
|
|
179
|
+
const cacheDir = (0, path_1.join)(((0, task_utils_1.findProjectRoot)() || process.cwd()), exports.UNITS_CACHE_DIR, iteration);
|
|
180
|
+
if (!await (0, fs_extra_1.pathExists)(cacheDir))
|
|
181
|
+
return [];
|
|
182
|
+
// v8.3.225+ 修复:排除 units.json 清单缓存(extractIterationUnits 产物,非单元分析结果),
|
|
183
|
+
// 否则数组被 push 进 results 后 sort 时 a.id 为 undefined 崩溃
|
|
184
|
+
const files = (await (0, fs_extra_1.readdir)(cacheDir)).filter(f => f.endsWith('.json') && f !== 'units.json');
|
|
185
|
+
const results = [];
|
|
186
|
+
for (const f of files) {
|
|
187
|
+
try {
|
|
188
|
+
const content = await (0, fs_extra_1.readFile)((0, path_1.join)(cacheDir, f), 'utf-8');
|
|
189
|
+
const parsed = JSON.parse(content);
|
|
190
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !parsed.id)
|
|
191
|
+
continue;
|
|
192
|
+
results.push(parsed);
|
|
193
|
+
}
|
|
194
|
+
catch { /* skip */ }
|
|
195
|
+
}
|
|
196
|
+
return results.sort((a, b) => a.id.localeCompare(b.id));
|
|
197
|
+
}
|
|
198
|
+
/** 检测未完成的单元分析 */
|
|
199
|
+
async function detectPendingUnits(iteration, allUnits) {
|
|
200
|
+
const completed = await loadUnitAnalyses(iteration);
|
|
201
|
+
const completedIds = new Set(completed.map(a => a.id));
|
|
202
|
+
// v8.3.266+: 用绝对路径迭代目录替代相对路径拼接——cwd 不在迭代父目录时旧写法会指向错误路径,
|
|
203
|
+
// 导致 unitHasSkeletonOutput 兜底检查全部误判为骨架(路径不存在)
|
|
204
|
+
const iterDir = await (0, context_1.getIterationDir)(iteration);
|
|
205
|
+
const pending = [];
|
|
206
|
+
for (const u of allUnits) {
|
|
207
|
+
if (!completedIds.has(u.id)) {
|
|
208
|
+
pending.push(u);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// v8.3.233+: 完成记录存在但落盘内容仍为骨架 → 仍视为待分析(兜底:
|
|
212
|
+
// 用户可对有问题的单元再次分析,直到无空骨架)
|
|
213
|
+
if (await unitHasSkeletonOutput(iterDir, u))
|
|
214
|
+
pending.push(u);
|
|
215
|
+
}
|
|
216
|
+
return pending;
|
|
217
|
+
}
|
|
218
|
+
/** v8.3.233+: 检查单元落盘规格是否仍为骨架/空内容(用于 pending 判定兜底,不写缓存) */
|
|
219
|
+
async function unitHasSkeletonOutput(iterDir, unit) {
|
|
220
|
+
const specDir = getUnitSpecDir(iterDir, unit);
|
|
221
|
+
for (const f of ['REQUIREMENT.md', 'ANALYSIS.md', 'TECH.md']) {
|
|
222
|
+
const fp = (0, path_1.join)(specDir, f);
|
|
223
|
+
if (!(await (0, fs_extra_1.pathExists)(fp)))
|
|
224
|
+
continue;
|
|
225
|
+
const content = (await (0, fs_extra_1.readFile)(fp, 'utf-8')).trim();
|
|
226
|
+
if (content && !content.includes('SPEC-SKELETON') && content.length >= 200)
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return true; // 三个候选全无效 → 视为骨架状态
|
|
230
|
+
}
|
|
231
|
+
/** 汇总所有单元分析为统一报告(带 [DOC:xxx] 标记) */
|
|
232
|
+
async function consolidateUnitAnalyses(iteration, units, analyses) {
|
|
233
|
+
const lines = [];
|
|
234
|
+
lines.push(`# ${iteration} 迭代 Spec 统一报告`);
|
|
235
|
+
lines.push(`> 生成时间: ${new Date().toISOString()}`);
|
|
236
|
+
lines.push(`> 共 ${analyses.length}/${units.length} 个功能单元已完成分析`);
|
|
237
|
+
lines.push('');
|
|
238
|
+
// 汇总 REQUIREMENT.md
|
|
239
|
+
lines.push('[DOC:REQUIREMENT.md]');
|
|
240
|
+
lines.push(`# ${iteration} 需求规格`);
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push('## 功能模块清单');
|
|
243
|
+
lines.push('| 单元ID | 模块名称 | 需求来源 |');
|
|
244
|
+
lines.push('|:---|:---|:---|');
|
|
245
|
+
for (const u of units) {
|
|
246
|
+
lines.push(`| ${u.id} | ${u.name} | ${u.source}#L${u.sourceRange.startLine} |`);
|
|
247
|
+
}
|
|
248
|
+
lines.push('');
|
|
249
|
+
for (const a of analyses) {
|
|
250
|
+
lines.push(`### ${a.id} ${a.name}`);
|
|
251
|
+
lines.push(a.req.split(/\r?\n/).slice(0, 5).join('\n'));
|
|
252
|
+
lines.push('');
|
|
253
|
+
}
|
|
254
|
+
lines.push('');
|
|
255
|
+
// 汇总 ANALYSIS.md
|
|
256
|
+
lines.push('[DOC:ANALYSIS.md]');
|
|
257
|
+
lines.push(`# ${iteration} 架构分析`);
|
|
258
|
+
lines.push('');
|
|
259
|
+
for (const a of analyses) {
|
|
260
|
+
lines.push(`## ${a.id} ${a.name}`);
|
|
261
|
+
lines.push(a.tech);
|
|
262
|
+
lines.push('');
|
|
263
|
+
}
|
|
264
|
+
lines.push('');
|
|
265
|
+
// 汇总 FUNCTION_MAP.md
|
|
266
|
+
lines.push('[DOC:FUNCTION_MAP.md]');
|
|
267
|
+
lines.push(`# ${iteration} 功能映射`);
|
|
268
|
+
lines.push('');
|
|
269
|
+
lines.push('| 单元ID | 功能模块 | 涉及端 | 优先级 |');
|
|
270
|
+
lines.push('|:---|:---|:---|:---|');
|
|
271
|
+
for (const a of analyses) {
|
|
272
|
+
lines.push(`| ${a.id} | ${a.name} | ${(a.tech.match(/端[::]\s*(.+)/)?.[1] || '待定')} | P1 |`);
|
|
273
|
+
}
|
|
274
|
+
lines.push('');
|
|
275
|
+
// 汇总 TECH.md
|
|
276
|
+
lines.push('[DOC:TECH.md]');
|
|
277
|
+
lines.push(`# ${iteration} 技术方案`);
|
|
278
|
+
lines.push('');
|
|
279
|
+
for (const a of analyses) {
|
|
280
|
+
lines.push(`## ${a.id} ${a.name}`);
|
|
281
|
+
lines.push(a.tech);
|
|
282
|
+
lines.push('');
|
|
283
|
+
}
|
|
284
|
+
lines.push('');
|
|
285
|
+
// 汇总 TEST.md
|
|
286
|
+
lines.push('[DOC:TEST.md]');
|
|
287
|
+
lines.push(`# ${iteration} 测试方案`);
|
|
288
|
+
lines.push('');
|
|
289
|
+
for (const a of analyses) {
|
|
290
|
+
lines.push(`## ${a.id} ${a.name}`);
|
|
291
|
+
lines.push(a.test);
|
|
292
|
+
lines.push('');
|
|
293
|
+
}
|
|
294
|
+
lines.push('');
|
|
295
|
+
// 汇总 RISK.md
|
|
296
|
+
lines.push('[DOC:RISK.md]');
|
|
297
|
+
lines.push(`# ${iteration} 风险评估`);
|
|
298
|
+
lines.push('');
|
|
299
|
+
for (const a of analyses) {
|
|
300
|
+
if (a.risk) {
|
|
301
|
+
lines.push(`## ${a.id} ${a.name}`);
|
|
302
|
+
lines.push(a.risk);
|
|
303
|
+
lines.push('');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
lines.push('');
|
|
307
|
+
return lines.join('\n');
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* v8.3.249+: 构建「各端工程路径」段——从 CONSTITUTION.md 工程表解析各端源码路径。
|
|
311
|
+
* 单元级/全量分析 prompt 共用,避免 AI 自行翻表定位源码。解析失败时返回空串不阻断。
|
|
312
|
+
*/
|
|
313
|
+
async function buildProjectPathsSection() {
|
|
314
|
+
try {
|
|
315
|
+
const projectInfoMap = await (0, spec_paths_1.parseProjectInfo)();
|
|
316
|
+
if (projectInfoMap.size === 0)
|
|
317
|
+
return '';
|
|
318
|
+
const lines = [];
|
|
319
|
+
for (const [identifier, info] of projectInfoMap) {
|
|
320
|
+
lines.push(`- \`${identifier}\`(${info.projectType || '-'})→ \`${info.srcPath}\``);
|
|
321
|
+
}
|
|
322
|
+
return lines.join('\n') + '\n';
|
|
323
|
+
}
|
|
324
|
+
catch { /* 路径解析失败不阻断分析 */
|
|
325
|
+
return '';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** 构建单个功能单元的深度分析 Prompt */
|
|
329
|
+
async function buildUnitAnalysisPrompt(iterDir, iteration, unit, allUnits) {
|
|
330
|
+
const unitRef = { id: unit.id, name: unit.name, content: unit.content };
|
|
331
|
+
const allUnitRefs = allUnits.map(u => ({ id: u.id, name: u.name, content: u.content }));
|
|
332
|
+
const ctxResult = await (0, unit_context_assembler_1.assembleUnitContext)(iterDir, iteration, unitRef, allUnitRefs, {
|
|
333
|
+
maxTokens: 6000,
|
|
334
|
+
maxRelatedUnits: 3,
|
|
335
|
+
maxCodeFiles: 5,
|
|
336
|
+
useKnowledgeGraph: true,
|
|
337
|
+
});
|
|
338
|
+
const prompt = `
|
|
339
|
+
# 功能单元深度分析: ${unit.id} ${unit.name}
|
|
340
|
+
|
|
341
|
+
## 迭代信息
|
|
342
|
+
- 迭代: ${iteration}
|
|
343
|
+
- 单元ID: ${unit.id}
|
|
344
|
+
- 来源: ${unit.source}#L${unit.sourceRange.startLine}
|
|
345
|
+
- 全迭代共 ${allUnits.length} 个功能单元
|
|
346
|
+
- 上下文预算: ${ctxResult.stats.estimatedTokens} tokens (核心${ctxResult.stats.coreChars}字符 + 关联${ctxResult.stats.relatedUnitChars + ctxResult.stats.codeChars}字符 + 全局${ctxResult.stats.globalChars}字符)
|
|
347
|
+
|
|
348
|
+
## 各端工程路径(结合源码分析时按此定位)
|
|
349
|
+
${await buildProjectPathsSection()}
|
|
350
|
+
|
|
351
|
+
## 需求上下文
|
|
352
|
+
${ctxResult.context}
|
|
353
|
+
|
|
354
|
+
## 分析要求
|
|
355
|
+
|
|
356
|
+
请对「${unit.name}」进行深度分析,输出以下文档(使用 [DOC:xxx] 标记分隔):
|
|
357
|
+
|
|
358
|
+
### [DOC:REQ.md] 需求规格
|
|
359
|
+
- 功能描述(用户故事格式)
|
|
360
|
+
- 输入/输出定义
|
|
361
|
+
- 业务规则(R-XX 编号)
|
|
362
|
+
- 异常场景
|
|
363
|
+
|
|
364
|
+
### [DOC:TECH.md] 技术方案
|
|
365
|
+
- 数据模型/接口设计
|
|
366
|
+
- 核心算法/逻辑
|
|
367
|
+
- 端分工(backend/web/admin 等)
|
|
368
|
+
- 依赖服务
|
|
369
|
+
|
|
370
|
+
### [DOC:TEST.md] 测试方案
|
|
371
|
+
- 验收标准(Given-When-Then)
|
|
372
|
+
- 边界值
|
|
373
|
+
- 异常路径
|
|
374
|
+
|
|
375
|
+
### [DOC:RISK.md] 风险评估(如适用)
|
|
376
|
+
- 技术风险
|
|
377
|
+
- 业务风险
|
|
378
|
+
- 缓解方案
|
|
379
|
+
|
|
380
|
+
## 输出格式
|
|
381
|
+
\`\`\`
|
|
382
|
+
[DOC:REQ.md]
|
|
383
|
+
(内容...)
|
|
384
|
+
|
|
385
|
+
[DOC:TECH.md]
|
|
386
|
+
(内容...)
|
|
387
|
+
|
|
388
|
+
[DOC:TEST.md]
|
|
389
|
+
(内容...)
|
|
390
|
+
|
|
391
|
+
[DOC:RISK.md]
|
|
392
|
+
(内容...)
|
|
393
|
+
\`\`\`
|
|
394
|
+
|
|
395
|
+
## 重要提示
|
|
396
|
+
- 只分析当前功能单元「${unit.name}」,不要涉及其他模块
|
|
397
|
+
- 每个文档必须有实质内容,不能是空模板
|
|
398
|
+
- 使用具体的技术术语,避免泛泛而谈
|
|
399
|
+
`;
|
|
400
|
+
return prompt.trim();
|
|
401
|
+
}
|
|
402
|
+
/** 单元名 → 目录名安全化(兜底模式下单元名可能是标题文本) */
|
|
403
|
+
function sanitizeUnitDirName(name) {
|
|
404
|
+
return name.replace(/[\\/:*?"<>|\s]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'unit';
|
|
405
|
+
}
|
|
406
|
+
/** 单元规格产物目录(对齐切分目录名,与 req-doc 澄清路径一致) */
|
|
407
|
+
function getUnitSpecDir(iterDir, unit) {
|
|
408
|
+
return (0, path_1.join)(iterDir, '020-specs', sanitizeUnitDirName(unit.name), 'overview');
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* 校验单元分析产物:020-specs/{单元名}/overview/ 下至少一份有效规格文档。
|
|
412
|
+
* 通过时顺带同步旧版单元分析缓存(consolidate 依赖),保持幂等。
|
|
413
|
+
*/
|
|
414
|
+
async function validateUnitSpec(iterDir, iter, unit) {
|
|
415
|
+
const specDir = getUnitSpecDir(iterDir, unit);
|
|
416
|
+
const candidates = ['REQUIREMENT.md', 'ANALYSIS.md', 'TECH.md'];
|
|
417
|
+
const found = {};
|
|
418
|
+
for (const f of candidates) {
|
|
419
|
+
const fp = (0, path_1.join)(specDir, f);
|
|
420
|
+
if (!(await (0, fs_extra_1.pathExists)(fp)))
|
|
421
|
+
continue;
|
|
422
|
+
const content = (await (0, fs_extra_1.readFile)(fp, 'utf-8')).trim();
|
|
423
|
+
if (!content || content.includes('SPEC-SKELETON') || content.length < 200)
|
|
424
|
+
continue;
|
|
425
|
+
found[f] = content;
|
|
426
|
+
}
|
|
427
|
+
if (Object.keys(found).length === 0) {
|
|
428
|
+
return `020-specs/${sanitizeUnitDirName(unit.name)}/overview/ 下缺少有效的规格文档(REQUIREMENT.md / ANALYSIS.md / TECH.md 至少一份,内容 ≥200 字符且非骨架模板)`;
|
|
429
|
+
}
|
|
430
|
+
// 同步旧版缓存,保证 --consolidate 可读
|
|
431
|
+
try {
|
|
432
|
+
await saveUnitAnalysis(iter, {
|
|
433
|
+
id: unit.id,
|
|
434
|
+
name: unit.name,
|
|
435
|
+
req: found['REQUIREMENT.md'] || '',
|
|
436
|
+
tech: found['TECH.md'] || found['ANALYSIS.md'] || '',
|
|
437
|
+
test: '',
|
|
438
|
+
timestamp: new Date().toISOString(),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch { /* 缓存同步失败不阻断主流程 */ }
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
/** 构建单元分析循环 prompt:复用现有 prompt + 落盘约束 + 重试反馈 */
|
|
445
|
+
async function buildUnitLoopPrompt(iterDir, iter, unit, allUnits, attempt, lastError) {
|
|
446
|
+
const base = await buildUnitAnalysisPrompt(iterDir, iter, unit, allUnits);
|
|
447
|
+
const unitDirName = sanitizeUnitDirName(unit.name);
|
|
448
|
+
const outputDir = `020-specs/${unitDirName}/overview/`;
|
|
449
|
+
let extra = `
|
|
450
|
+
|
|
451
|
+
## 落盘方式(重要 — 请直接写文件)
|
|
452
|
+
|
|
453
|
+
请使用 Write 工具将以下文档直接写入迭代目录(不要只在回复中输出内容):
|
|
454
|
+
|
|
455
|
+
- \`${outputDir}REQUIREMENT.md\` — 需求规格(功能描述/输入输出/业务规则/异常场景)
|
|
456
|
+
- \`${outputDir}ANALYSIS.md\` — 需求分析(现状/差距/方案要点)
|
|
457
|
+
- \`${outputDir}TECH.md\` — 技术方案(数据模型/接口设计/端分工/依赖)
|
|
458
|
+
|
|
459
|
+
约束:
|
|
460
|
+
- 每个文件内容不少于 200 字符,禁止输出空模板或骨架占位
|
|
461
|
+
- 写入前先创建目录;写完后回复一行:\`DONE ${unit.name}\`
|
|
462
|
+
- 若上下文中含「需求图片/原型」或 \`\` 图片引用,请先用 Read 工具打开图片文件查看,再结合图片信息撰写规格;不要忽略图片提示
|
|
463
|
+
`;
|
|
464
|
+
if (attempt > 1 && lastError) {
|
|
465
|
+
extra += `
|
|
466
|
+
## ⚠️ 上次尝试未通过校验(第 ${attempt - 1} 次尝试)
|
|
467
|
+
|
|
468
|
+
失败原因:${lastError}
|
|
469
|
+
|
|
470
|
+
请检查文件是否写入了正确路径(\`${outputDir}\`),并确保内容完整、非骨架模板。
|
|
471
|
+
`;
|
|
472
|
+
}
|
|
473
|
+
return base + extra;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* v8.3.266: 调度循环 SDK 派单——Qoder 环境非 TTY 下由 CLI 直接调起子 Agent
|
|
477
|
+
* (独立上下文、acceptEdits 直接写文件),返回 true 表示派单成功(含自动落盘)。
|
|
478
|
+
*/
|
|
479
|
+
async function trySdkDispatchUnit(prompt, iter, unitName) {
|
|
480
|
+
try {
|
|
481
|
+
const agentCtx = {
|
|
482
|
+
subagent: 'requirement-analyzer',
|
|
483
|
+
iteration: iter,
|
|
484
|
+
contextBudget: 6000,
|
|
485
|
+
contextType: 'incremental',
|
|
486
|
+
cwd: (0, task_utils_1.findProjectRoot)() || process.cwd(),
|
|
487
|
+
};
|
|
488
|
+
const dispatchResult = await (0, agent_adapter_1.dispatchSubagent)(agentCtx, prompt);
|
|
489
|
+
if (!dispatchResult || !dispatchResult.success) {
|
|
490
|
+
logger_1.logger.debug('[analyze] SDK 派单未成功,回退标记模式');
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
// v8.3.266: 防御空转——success 但既没写文件也没返回可落盘内容,视为无效派单,
|
|
494
|
+
// 立即回退标记模式(否则循环会白白耗尽该单元 3 次尝试并误标 failed)
|
|
495
|
+
if (!dispatchResult.content && (!dispatchResult.filesChanged || dispatchResult.filesChanged.length === 0)) {
|
|
496
|
+
logger_1.logger.warn('[analyze] SDK 派单无产出(未写文件且无回传内容),回退标记模式');
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
logger_1.logger.info(`[analyze] 调度循环子 Agent SDK 派单成功: ${unitName}`);
|
|
500
|
+
// v8.3.196+ 自动落盘:子 Agent 未直接写文件时,解析其返回的 JSON/[DOC:] 内容
|
|
501
|
+
// 并路由到该单元的 overview/ 目录(applyDocContent 无单元路由,此处专用落盘)
|
|
502
|
+
if (dispatchResult.content) {
|
|
503
|
+
const autoApplied = await applyUnitLoopDocs(dispatchResult.content, iter, unitName);
|
|
504
|
+
if (autoApplied > 0) {
|
|
505
|
+
process.stdout.write(`✅ 已自动写入 ${autoApplied} 个文档到 020-specs/${sanitizeUnitDirName(unitName)}/overview/\n`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (dispatchResult.filesChanged && dispatchResult.filesChanged.length > 0) {
|
|
509
|
+
process.stdout.write(`📄 文件变更: ${dispatchResult.filesChanged.join(', ')}\n`);
|
|
510
|
+
}
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
logger_1.logger.debug(`[analyze] SDK 派单异常: ${e.message},回退标记模式`);
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* v8.3.266: 把子 Agent 回传的 JSON/[DOC:] 内容写入指定单元的 overview/ 目录。
|
|
520
|
+
* 文档名归一化:analyze prompt 用 [DOC:REQ.md],落盘用 REQUIREMENT.md(validateUnitSpec 校验名单)。
|
|
521
|
+
*/
|
|
522
|
+
async function applyUnitLoopDocs(content, iter, unitName) {
|
|
523
|
+
const trimmed = content.trim();
|
|
524
|
+
if (!trimmed)
|
|
525
|
+
return 0;
|
|
526
|
+
const iterDir = await (0, context_1.getIterationDir)(iter);
|
|
527
|
+
const unitDir = (0, path_1.join)(iterDir, '020-specs', sanitizeUnitDirName(unitName), 'overview');
|
|
528
|
+
let docs = {};
|
|
529
|
+
if (trimmed.startsWith('{')) {
|
|
530
|
+
try {
|
|
531
|
+
const parsed = JSON.parse(trimmed);
|
|
532
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
533
|
+
docs = parsed;
|
|
534
|
+
}
|
|
535
|
+
catch { /* 回退 [DOC:] 标记 */ }
|
|
536
|
+
}
|
|
537
|
+
if (Object.keys(docs).length === 0) {
|
|
538
|
+
for (const [name, c] of (0, shared_1.parseDocMarkers)(trimmed))
|
|
539
|
+
docs[name] = c;
|
|
540
|
+
}
|
|
541
|
+
if (Object.keys(docs).length === 0)
|
|
542
|
+
return 0;
|
|
543
|
+
const NAME_MAP = { 'REQ.md': 'REQUIREMENT.md' };
|
|
544
|
+
let applied = 0;
|
|
545
|
+
for (const [filename, docContent] of Object.entries(docs)) {
|
|
546
|
+
if (typeof docContent !== 'string' || !docContent.trim())
|
|
547
|
+
continue;
|
|
548
|
+
const baseName = (filename.split('/').pop() || filename).trim();
|
|
549
|
+
const normalized = NAME_MAP[baseName] || baseName;
|
|
550
|
+
if (!normalized.endsWith('.md') || normalized.startsWith('.'))
|
|
551
|
+
continue;
|
|
552
|
+
await (0, fs_extra_1.ensureDir)(unitDir);
|
|
553
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(unitDir, normalized), docContent);
|
|
554
|
+
applied++;
|
|
555
|
+
}
|
|
556
|
+
return applied;
|
|
557
|
+
}
|
|
558
|
+
/** 单元分析调度循环:构建步骤清单并推进一轮 */
|
|
559
|
+
async function runUnitDispatchLoop(iter, iterDir, units) {
|
|
560
|
+
const steps = units.map((u) => ({
|
|
561
|
+
id: `unit:${sanitizeUnitDirName(u.name)}`,
|
|
562
|
+
name: u.name,
|
|
563
|
+
validate: () => validateUnitSpec(iterDir, iter, u),
|
|
564
|
+
buildPrompt: ({ attempt, lastError }) => buildUnitLoopPrompt(iterDir, iter, u, units, attempt, lastError),
|
|
565
|
+
}));
|
|
566
|
+
return (0, dispatch_loop_1.runDispatchLoopRound)(steps, {
|
|
567
|
+
projectRoot: (0, task_utils_1.findProjectRoot)() || process.cwd(),
|
|
568
|
+
scope: `analyze-units:${iter}`,
|
|
569
|
+
// v8.3.233+: 一轮内最多 3 次尝试(初始 + 2 重试);循环外 resume 还可无限续跑
|
|
570
|
+
maxRetries: 2,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/** 输出调度循环完成汇总 */
|
|
574
|
+
async function printUnitLoopSummary(iter, iterDir, units, result) {
|
|
575
|
+
const s = result.summary;
|
|
576
|
+
if (s.failed.length > 0) {
|
|
577
|
+
logger_1.logger.warn(`⚠️ 功能单元分析循环完成: ${s.done.length} 完成 / ${s.failed.length} 失败`);
|
|
578
|
+
for (const id of s.failed) {
|
|
579
|
+
logger_1.logger.warn(` ❌ ${id}: ${s.failedErrors[id] || '未知原因'}`);
|
|
580
|
+
}
|
|
581
|
+
logger_1.logger.info(` 重跑失败单元: speccore analyze --unit "<单元名>" --prompt -I ${iter}`);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
logger_1.logger.success(`🎉 所有 ${s.done.length} 个功能单元分析完成!`);
|
|
585
|
+
}
|
|
586
|
+
logger_1.logger.info(` 下一步: 汇总统一报告 speccore analyze --prompt -I ${iter} --consolidate`);
|
|
587
|
+
// v8.3.266+: 循环只保证 overview/ 3 份,端级文档({feature}/{端}/)由 Phase 2 负责——
|
|
588
|
+
// 此前完成提示不提 Phase 2,用户以为分析已闭环,taskify 时端级内容仍是骨架
|
|
589
|
+
const platformSk = await countPlatformSkeletons(iterDir);
|
|
590
|
+
if (platformSk.total > 0 && platformSk.skeleton > 0) {
|
|
591
|
+
logger_1.logger.warn(`⚠️ 端级文档仍有 ${platformSk.skeleton}/${platformSk.total} 份为骨架(overview/ 之外)`);
|
|
592
|
+
logger_1.logger.info(` Phase 2 生成各端专属文档: speccore analyze --prompt -I ${iter} --phase 2`);
|
|
593
|
+
}
|
|
594
|
+
// v8.3.234+: 执行完成质量报告(成功/失败清单 + 产出物评分)落盘 QUALITY_AUDIT.md
|
|
595
|
+
await buildAndSaveQualityReport(iterDir, iter, units);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* v8.3.266+: 统计 020-specs/{feature}/{端}/ 下仍为骨架的 .md 文件数。
|
|
599
|
+
* 用于调度循环完成后引导 Phase 2(调度循环只保证 overview/ 3 份落盘)。
|
|
600
|
+
*/
|
|
601
|
+
async function countPlatformSkeletons(iterDir) {
|
|
602
|
+
let skeleton = 0;
|
|
603
|
+
let total = 0;
|
|
604
|
+
try {
|
|
605
|
+
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
606
|
+
if (!(await (0, fs_extra_1.pathExists)(specDir)))
|
|
607
|
+
return { skeleton, total };
|
|
608
|
+
const features = await (0, spec_paths_1.parseFeatureList)(iterDir);
|
|
609
|
+
const platforms = await (0, spec_paths_1.parsePlatformList)();
|
|
610
|
+
// v8.3.278+: 统计口径与骨架生成一致——只统计声明端(未声明单元全端兜底),
|
|
611
|
+
// 避免把不涉及端的旧残留目录计入骨架提示
|
|
612
|
+
const featurePlatforms = await (0, spec_skeleton_1.resolveFeaturePlatformMap)((0, path_1.join)(iterDir, '010-requirements', 'features'), features, platforms);
|
|
613
|
+
for (const feature of features) {
|
|
614
|
+
const declared = featurePlatforms.get(feature);
|
|
615
|
+
const unitPlatforms = declared && declared.length > 0 ? declared : platforms;
|
|
616
|
+
for (const platform of unitPlatforms) {
|
|
617
|
+
const pDir = (0, path_1.join)(specDir, feature, platform);
|
|
618
|
+
if (!(await (0, fs_extra_1.pathExists)(pDir)))
|
|
619
|
+
continue;
|
|
620
|
+
const entries = await (0, fs_extra_1.readdir)(pDir, { withFileTypes: true }).catch(() => []);
|
|
621
|
+
for (const entry of entries) {
|
|
622
|
+
if (!entry.name.endsWith('.md') || !entry.isFile())
|
|
623
|
+
continue;
|
|
624
|
+
total++;
|
|
625
|
+
const content = (await (0, fs_extra_1.readFile)((0, path_1.join)(pDir, entry.name), 'utf-8').catch(() => '')).trim();
|
|
626
|
+
if (!content || content.includes('SPEC-SKELETON') || content.length < 200)
|
|
627
|
+
skeleton++;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch { /* 统计失败不阻断主流程 */ }
|
|
633
|
+
return { skeleton, total };
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* v8.3.234+: 生成并落盘「功能单元分析质量报告」——成功/失败清单 + 每单元产出物评分。
|
|
637
|
+
* 落盘到 020-specs/QUALITY_AUDIT.md;无可评单元时返回 null。
|
|
638
|
+
*/
|
|
639
|
+
async function buildAndSaveQualityReport(iterDir, iter, units) {
|
|
640
|
+
if (units.length === 0)
|
|
641
|
+
return null;
|
|
642
|
+
const { validateContentQuality } = await Promise.resolve().then(() => __importStar(require('../../core/spec-skeleton')));
|
|
643
|
+
const report = [];
|
|
644
|
+
for (const u of units) {
|
|
645
|
+
const specDir = getUnitSpecDir(iterDir, u);
|
|
646
|
+
const rows = [];
|
|
647
|
+
for (const f of ['REQUIREMENT.md', 'ANALYSIS.md', 'TECH.md']) {
|
|
648
|
+
const fp = (0, path_1.join)(specDir, f);
|
|
649
|
+
if (!(await (0, fs_extra_1.pathExists)(fp)))
|
|
650
|
+
continue;
|
|
651
|
+
const content = (await (0, fs_extra_1.readFile)(fp, 'utf-8')).trim();
|
|
652
|
+
if (!content || content.includes('SPEC-SKELETON') || content.length < 200)
|
|
653
|
+
continue;
|
|
654
|
+
const q = await validateContentQuality(f, content, '');
|
|
655
|
+
rows.push({ doc: f, score: q.score, issues: q.issues });
|
|
656
|
+
}
|
|
657
|
+
report.push({ unit: u.name, ok: rows.length > 0, rows });
|
|
658
|
+
}
|
|
659
|
+
const okCount = report.filter(r => r.ok).length;
|
|
660
|
+
const failCount = report.length - okCount;
|
|
661
|
+
const totalDocs = report.reduce((a, r) => a + r.rows.length, 0);
|
|
662
|
+
const avgScore = totalDocs > 0 ? Math.round(report.reduce((a, r) => a + r.rows.reduce((b, row) => b + row.score, 0), 0) / totalDocs) : 0;
|
|
663
|
+
const lines = [];
|
|
664
|
+
lines.push('# 功能单元分析质量报告');
|
|
665
|
+
lines.push('');
|
|
666
|
+
lines.push(`> 生成时间: ${new Date().toISOString()}`);
|
|
667
|
+
lines.push(`> 迭代: ${iter}`);
|
|
668
|
+
lines.push(`> 结果: ${okCount} 成功 / ${failCount} 失败(共 ${units.length} 单元,评 ${totalDocs} 份文档,平均 ${avgScore} 分)`);
|
|
669
|
+
lines.push('');
|
|
670
|
+
lines.push('## 评分明细');
|
|
671
|
+
lines.push('');
|
|
672
|
+
lines.push('| 单元 | 状态 | 文档 | 评分 | 主要扣分项 |');
|
|
673
|
+
lines.push('|:---|:---|:---|:---|:---|');
|
|
674
|
+
for (const r of report) {
|
|
675
|
+
if (r.ok) {
|
|
676
|
+
for (const row of r.rows) {
|
|
677
|
+
lines.push(`| ${r.unit} | ✅ | ${row.doc} | ${row.score} | ${row.issues.slice(0, 3).join(';') || '—'} |`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
lines.push(`| ${r.unit} | ❌ 失败 | — | — | 缺少有效规格文档(REQUIREMENT/ANALYSIS/TECH 至少一份,内容 ≥200 字符且非骨架) |`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
lines.push('');
|
|
685
|
+
lines.push('## 失败单元修复');
|
|
686
|
+
lines.push('');
|
|
687
|
+
if (failCount === 0) {
|
|
688
|
+
lines.push('无失败单元。');
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
lines.push('```bash');
|
|
692
|
+
lines.push(`speccore analyze --resume-units -I ${iter}`);
|
|
693
|
+
lines.push('```');
|
|
694
|
+
lines.push('仍为骨架/空内容的单元会被重新纳入分析,循环直到干净。');
|
|
695
|
+
}
|
|
696
|
+
lines.push('');
|
|
697
|
+
const specRoot = (0, path_1.join)(iterDir, '020-specs');
|
|
698
|
+
await (0, fs_extra_1.ensureDir)(specRoot);
|
|
699
|
+
const reportPath = (0, path_1.join)(specRoot, 'QUALITY_AUDIT.md');
|
|
700
|
+
await (0, fs_extra_1.writeFile)(reportPath, lines.join('\n'));
|
|
701
|
+
logger_1.logger.info(` 📊 质量报告已落盘: 020-specs/QUALITY_AUDIT.md(${okCount} 成功 / ${failCount} 失败,平均 ${avgScore} 分)`);
|
|
702
|
+
return reportPath;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* 功能单元聚焦分析主入口
|
|
706
|
+
* v8.3.341+: 从 analyzeCommand 抽出的模式早退块
|
|
707
|
+
*/
|
|
708
|
+
async function analyzeUnitModes(options, projectRoot) {
|
|
709
|
+
const iter = options.iteration || await (0, context_1.getDefaultIteration)();
|
|
710
|
+
if (!iter) {
|
|
711
|
+
logger_1.logger.error('请指定迭代: -I <iteration>');
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const iterDir = await (0, context_1.getIterationDir)(iter);
|
|
715
|
+
// Step 1: 提取功能单元清单
|
|
716
|
+
let units = [];
|
|
717
|
+
const unitsCachePath = (0, path_1.join)(projectRoot, exports.UNITS_CACHE_DIR, iter, 'units.json');
|
|
718
|
+
if (await (0, fs_extra_1.pathExists)(unitsCachePath)) {
|
|
719
|
+
try {
|
|
720
|
+
units = JSON.parse(await (0, fs_extra_1.readFile)(unitsCachePath, 'utf-8'));
|
|
721
|
+
logger_1.logger.info(`📋 已加载功能单元清单: ${units.length} 个`);
|
|
722
|
+
}
|
|
723
|
+
catch { /* fall through to re-extract */ }
|
|
724
|
+
}
|
|
725
|
+
if (units.length === 0 || options.extractUnits) {
|
|
726
|
+
units = await extractIterationUnits(iterDir);
|
|
727
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(unitsCachePath));
|
|
728
|
+
await (0, fs_extra_1.writeFile)(unitsCachePath, JSON.stringify(units, null, 2));
|
|
729
|
+
logger_1.logger.info(`📋 功能单元清单已提取: ${units.length} 个`);
|
|
730
|
+
for (const u of units) {
|
|
731
|
+
logger_1.logger.info(` ${u.id}: ${u.name} (${u.source}#L${u.sourceRange.startLine})`);
|
|
732
|
+
}
|
|
733
|
+
if (options.extractUnits) {
|
|
734
|
+
logger_1.logger.info('');
|
|
735
|
+
logger_1.logger.info('📋 下一步: 逐个分析功能单元');
|
|
736
|
+
logger_1.logger.info(` speccore analyze --prompt -I ${iter} --unit M-01`);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else if (options.autoUnits || options.resumeUnits) {
|
|
741
|
+
// v8.3.266+: 缓存与磁盘 features/ 权威源对照——旧缓存可能含 unitize 重切前的假单元
|
|
742
|
+
// (实测会议项目 23 个缓存单元中 6 个在 features/ 不存在),调度循环会给假单元生成
|
|
743
|
+
// Prompt 写到 features 锚点之外的目录,白写且 taskify/Phase 2 均不读。
|
|
744
|
+
const diskUnits = await extractIterationUnits(iterDir);
|
|
745
|
+
const cacheNames = new Set(units.map((u) => u.name));
|
|
746
|
+
const diskNames = new Set(diskUnits.map((u) => u.name));
|
|
747
|
+
const mismatched = diskUnits.length > 0
|
|
748
|
+
&& (diskUnits.length !== units.length || [...diskNames].some((n) => !cacheNames.has(n)));
|
|
749
|
+
if (mismatched) {
|
|
750
|
+
units = diskUnits;
|
|
751
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(unitsCachePath));
|
|
752
|
+
await (0, fs_extra_1.writeFile)(unitsCachePath, JSON.stringify(units, null, 2));
|
|
753
|
+
logger_1.logger.warn(`⚠️ 单元清单与磁盘 features/ 目录不一致,已刷新: ${units.length} 个`);
|
|
754
|
+
for (const u of units) {
|
|
755
|
+
logger_1.logger.info(` ${u.id}: ${u.name} (${u.source}#L${u.sourceRange.startLine})`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// Step 2: 汇总模式(--consolidate)
|
|
760
|
+
if (options.consolidate) {
|
|
761
|
+
const analyses = await loadUnitAnalyses(iter);
|
|
762
|
+
if (analyses.length === 0) {
|
|
763
|
+
logger_1.logger.warn('⚠️ 尚未完成任何单元分析,请先运行 --unit 模式');
|
|
764
|
+
logger_1.logger.info(` speccore analyze --prompt -I ${iter} --unit M-01`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const report = await consolidateUnitAnalyses(iter, units, analyses);
|
|
768
|
+
if (options.prompt) {
|
|
769
|
+
await (0, shared_1.emitAiPrompt)(iter, `${report}\n\n## 操作选项\n\n**选项 A:确认通过,写入所有文档**\n\`\`\`bash\nspeccore analyze --apply '[DOC:...]' -I ${iter}\n\`\`\`\n`, { mode: 'consolidate-report' });
|
|
770
|
+
process.exitCode = 10;
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (options.apply) {
|
|
774
|
+
// 直接通过 --apply 的 [DOC:xxx] 处理逻辑写入
|
|
775
|
+
logger_1.logger.info(`📋 汇总报告已生成,共 ${analyses.length}/${units.length} 个单元`);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
logger_1.logger.info(`📋 汇总报告(${analyses.length}/${units.length} 个单元)`);
|
|
779
|
+
logger_1.logger.info(' 使用 --prompt 查看完整报告,或 --apply 直接写入');
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
// Step 3: 确定目标功能单元集合(--auto-units / --resume-units / --unit 可逗号分隔)
|
|
783
|
+
let targetUnitIds = [];
|
|
784
|
+
// 3a. --auto-units: 自动遍历所有待分析单元
|
|
785
|
+
if (options.autoUnits) {
|
|
786
|
+
const pending = await detectPendingUnits(iter, units);
|
|
787
|
+
if (pending.length === 0) {
|
|
788
|
+
logger_1.logger.success('🎉 所有功能单元分析已完成!');
|
|
789
|
+
logger_1.logger.info(' 运行 --consolidate 汇总为统一报告');
|
|
790
|
+
logger_1.logger.info(` speccore analyze --prompt -I ${iter} --consolidate`);
|
|
791
|
+
// v8.3.234+: 执行完成质量报告(成功/失败清单 + 产出物评分)
|
|
792
|
+
await buildAndSaveQualityReport(iterDir, iter, units);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
targetUnitIds = pending.map(u => u.id);
|
|
796
|
+
logger_1.logger.info(`🚀 自动遍历 ${targetUnitIds.length} 个待分析单元: ${targetUnitIds.join(', ')}`);
|
|
797
|
+
}
|
|
798
|
+
// 3b. --resume-units: 断点续跑(取第一个待分析单元)
|
|
799
|
+
if (options.resumeUnits) {
|
|
800
|
+
const pending = await detectPendingUnits(iter, units);
|
|
801
|
+
if (pending.length === 0) {
|
|
802
|
+
logger_1.logger.success('🎉 所有功能单元分析已完成!');
|
|
803
|
+
logger_1.logger.info(' 运行 --consolidate 汇总为统一报告');
|
|
804
|
+
logger_1.logger.info(` speccore analyze --prompt -I ${iter} --consolidate`);
|
|
805
|
+
// v8.3.234+: 执行完成质量报告(成功/失败清单 + 产出物评分)
|
|
806
|
+
await buildAndSaveQualityReport(iterDir, iter, units);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
targetUnitIds = [pending[0].id];
|
|
810
|
+
logger_1.logger.info(`🔄 断点续跑: ${pending[0].id} ${pending[0].name}(剩余 ${pending.length} 个)`);
|
|
811
|
+
}
|
|
812
|
+
// 3d. v8.3.225+: --auto-units / --resume-units 调度循环模式(方式 Y 宿主会话内闭环)
|
|
813
|
+
// CLI 每轮推进状态机:校验产出 → 已就绪自动标记并推进,未就绪输出该单元 prompt(exitCode 10)
|
|
814
|
+
// → 宿主 AI 落盘 → 再次调用本命令 → CLI 校验通过自动推进下一个单元,直到全部完成
|
|
815
|
+
if (options.autoUnits || options.resumeUnits) {
|
|
816
|
+
// v8.3.266: Qoder 环境非 TTY 下优先 SDK 派单——CLI 进程内直接调起子 Agent
|
|
817
|
+
// (独立上下文、acceptEdits 直接写文件),校验通过后自动推进下一单元,宿主全程
|
|
818
|
+
// 不参与;SDK 不可用/失败时回退标记模式(exitCode 10 宿主接力)。
|
|
819
|
+
// 收敛性:每轮单元状态必然前进(done/failed)或 SDK 失败退出,不会死循环。
|
|
820
|
+
const sdkSupported = !process.stdout.isTTY && (0, agent_adapter_1.isSdkDispatchSupported)();
|
|
821
|
+
const maxDispatchRounds = units.length * 3 + 5; // 防御性上限(每单元最多 3 次尝试 + 余量)
|
|
822
|
+
let dispatchCount = 0;
|
|
823
|
+
while (true) {
|
|
824
|
+
const loopResult = await runUnitDispatchLoop(iter, iterDir, units);
|
|
825
|
+
if (loopResult.finished) {
|
|
826
|
+
await printUnitLoopSummary(iter, iterDir, units, loopResult);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const fallbackPrompt = `${loopResult.prompt}\n\n## 完成后继续\n\n落盘后运行以下命令,CLI 将校验产出并自动推进到下一个单元:\n\n\`\`\`bash\nspeccore analyze --resume-units -I ${iter}\n\`\`\`\n`;
|
|
830
|
+
if (!sdkSupported || !(await trySdkDispatchUnit(loopResult.prompt, iter, loopResult.step.name))) {
|
|
831
|
+
// 回退标记模式(非 Qoder 宿主 / TTY / SDK 失败均走此路径)
|
|
832
|
+
await (0, shared_1.emitAiPrompt)(iter, fallbackPrompt, { mode: 'resume-units-loop' });
|
|
833
|
+
process.exitCode = 10;
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (++dispatchCount >= maxDispatchRounds) {
|
|
837
|
+
logger_1.logger.warn(`⚠️ SDK 派单轮次超限(${maxDispatchRounds}),退出调度循环`);
|
|
838
|
+
process.exitCode = 10;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// 理论不可达:while(true) 内所有路径均 return
|
|
843
|
+
}
|
|
844
|
+
// 3c. --unit: 指定单元(支持逗号分隔多个,如 M-01,M-03)
|
|
845
|
+
if (options.unit) {
|
|
846
|
+
targetUnitIds = options.unit.split(',').map(s => s.trim()).filter(Boolean);
|
|
847
|
+
// v8.3.249+: 支持目录名直接指定——unitize 切分的目录名(如 v3-service-01-门禁授权同步)
|
|
848
|
+
// 与缓存 M-ID 体系无映射,仅按 u.id 匹配会让用户面对目录名无法指定单元
|
|
849
|
+
const matchUnit = (q, list) => list.some(u => u.id === q || u.name === q);
|
|
850
|
+
let invalid = targetUnitIds.filter(id => !matchUnit(id, units));
|
|
851
|
+
// v8.3.249+: unitize/doc2spec 切分不写 units.json 缓存,新切分的单元不在缓存清单中——
|
|
852
|
+
// 回退磁盘提取(features/ 目录)合并后再匹配,避免「刚切完就找不到」的断层
|
|
853
|
+
if (invalid.length > 0) {
|
|
854
|
+
const diskUnits = await extractIterationUnits(iterDir);
|
|
855
|
+
if (diskUnits.length > units.length) {
|
|
856
|
+
units = diskUnits;
|
|
857
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(unitsCachePath));
|
|
858
|
+
await (0, fs_extra_1.writeFile)(unitsCachePath, JSON.stringify(units, null, 2));
|
|
859
|
+
logger_1.logger.info(`📋 检测到新切分单元,清单已刷新: ${units.length} 个`);
|
|
860
|
+
invalid = targetUnitIds.filter(id => !matchUnit(id, units));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (invalid.length > 0) {
|
|
864
|
+
logger_1.logger.error(`未找到功能单元: ${invalid.join(', ')}`);
|
|
865
|
+
logger_1.logger.info(` 可用单元: ${units.map(u => u.id).join(', ')}`);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const targetUnits = targetUnitIds
|
|
870
|
+
.map(id => units.find(u => u.id === id || u.name === id))
|
|
871
|
+
.filter((u) => !!u);
|
|
872
|
+
// 单单元:保持原有行为
|
|
873
|
+
if (targetUnits.length === 1) {
|
|
874
|
+
const targetUnit = targetUnits[0];
|
|
875
|
+
if (options.prompt) {
|
|
876
|
+
const prompt = await buildUnitAnalysisPrompt(iterDir, iter, targetUnit, units);
|
|
877
|
+
await (0, shared_1.emitAiPrompt)(iter, prompt, { mode: 'unit', unit: targetUnit.id });
|
|
878
|
+
process.exitCode = 10;
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (options.apply) {
|
|
882
|
+
// v8.3.249+: 此前为“空实现”——只打印「已接收」但 [DOC:xxx] 内容从不落盘,
|
|
883
|
+
// AI 认真提交后产物丢失,taskify 拿不到分析结果。现在真正解析写入。
|
|
884
|
+
await (0, ai_trace_1.recordAiTrace)(iter, 'apply-in', {
|
|
885
|
+
unit: targetUnit.id,
|
|
886
|
+
rawLength: options.apply.length,
|
|
887
|
+
hasDocMarkers: /\[DOC:[^\]]+\]/.test(options.apply),
|
|
888
|
+
rawPreview: options.apply.trim().slice(0, 500),
|
|
889
|
+
});
|
|
890
|
+
const docBlocks = (0, shared_1.parseDocMarkers)(options.apply);
|
|
891
|
+
if (docBlocks.size === 0) {
|
|
892
|
+
await (0, ai_trace_1.recordAiTrace)(iter, 'apply-fail', { unit: targetUnit.id, reason: 'no-doc-markers', rawInput: options.apply });
|
|
893
|
+
logger_1.logger.warn('⚠️ 未检测到 [DOC:xxx] 标记,请按 prompt 格式输出');
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const specDir = getUnitSpecDir(iterDir, targetUnit);
|
|
897
|
+
await (0, fs_extra_1.ensureDir)(specDir);
|
|
898
|
+
let written = 0;
|
|
899
|
+
const writtenFiles = [];
|
|
900
|
+
for (const [docName, content] of docBlocks) {
|
|
901
|
+
const safeName = (0, path_1.basename)(docName);
|
|
902
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*\.(md|yaml)$/.test(safeName))
|
|
903
|
+
continue;
|
|
904
|
+
if (content.trim().length < 50) {
|
|
905
|
+
logger_1.logger.warn(` ⏭️ 跳过 ${safeName}(内容过短)`);
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(specDir, safeName), content.trim());
|
|
909
|
+
written++;
|
|
910
|
+
writtenFiles.push(safeName);
|
|
911
|
+
}
|
|
912
|
+
await (0, ai_trace_1.recordAiTrace)(iter, 'apply-done', { unit: targetUnit.id, applied: written, files: writtenFiles });
|
|
913
|
+
logger_1.logger.success(`✅ 功能单元 ${targetUnit.id} 分析结果已写入 ${written} 个文档`);
|
|
914
|
+
logger_1.logger.info(` 📂 020-specs/${sanitizeUnitDirName(targetUnit.name)}/overview/`);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
logger_1.logger.info(`📋 功能单元: ${targetUnit.id} ${targetUnit.name}`);
|
|
918
|
+
logger_1.logger.info(` 来源: ${targetUnit.source}#L${targetUnit.sourceRange.startLine}`);
|
|
919
|
+
logger_1.logger.info(' 使用 --prompt 生成分析 Prompt,或 --apply 写入分析结果');
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
// 多单元:逐个输出独立 Prompt(保持每个单元独立上下文,步骤隔离)
|
|
923
|
+
if (targetUnits.length > 1) {
|
|
924
|
+
if (options.prompt) {
|
|
925
|
+
const parts = [];
|
|
926
|
+
parts.push(`# 功能单元批量分析指令(${targetUnits.length} 个单元)`);
|
|
927
|
+
parts.push('');
|
|
928
|
+
parts.push('> ⚠️ **步骤隔离要求**:每个功能单元独立分析、独立提交,不要跨单元混合内容。');
|
|
929
|
+
parts.push('> 请**逐个单元**处理:每完成一个单元,立即用对应命令提交该单元结果,再处理下一个。');
|
|
930
|
+
parts.push('');
|
|
931
|
+
for (const u of targetUnits) {
|
|
932
|
+
const p = await buildUnitAnalysisPrompt(iterDir, iter, u, units);
|
|
933
|
+
parts.push(`\n\n===== 功能单元 ${u.id}: ${u.name} =====\n`);
|
|
934
|
+
parts.push(p);
|
|
935
|
+
parts.push(`\n> 提交本单元: speccore analyze --unit ${u.id} --apply '[DOC:...]' -I ${iter}\n`);
|
|
936
|
+
}
|
|
937
|
+
process.stdout.write(parts.join('\n'));
|
|
938
|
+
process.exitCode = 10;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (options.apply) {
|
|
942
|
+
logger_1.logger.warn('⚠️ 多单元模式下 --apply 请逐个单元执行(指定单个 --unit)');
|
|
943
|
+
logger_1.logger.info(` 待分析单元: ${targetUnits.map(u => u.id).join(', ')}`);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
logger_1.logger.info(`📋 已选择 ${targetUnits.length} 个功能单元: ${targetUnits.map(u => u.id).join(', ')}`);
|
|
947
|
+
logger_1.logger.info(' 使用 --prompt 逐个生成分析 Prompt,或 --auto-units 自动遍历全部待分析单元');
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
//# sourceMappingURL=units.js.map
|