@gordon.gan/specflow 1.8.0-beta → 1.8.1-beta

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 (179) hide show
  1. package/README.md +7 -5
  2. package/dist/cli/commands/document-run.d.ts +97 -0
  3. package/dist/cli/commands/document-run.js +845 -0
  4. package/dist/cli/index.js +2 -2
  5. package/dist/core/artifact-language.js +11 -1
  6. package/dist/core/document/chapters.d.ts +6 -0
  7. package/dist/core/document/chapters.js +36 -0
  8. package/dist/core/document/coverage.d.ts +25 -0
  9. package/dist/core/document/coverage.js +49 -0
  10. package/dist/core/document/digests.d.ts +40 -0
  11. package/dist/core/document/digests.js +144 -0
  12. package/dist/core/document/engine.d.ts +101 -0
  13. package/dist/core/document/engine.js +722 -0
  14. package/dist/core/document/extract.d.ts +61 -0
  15. package/dist/core/document/extract.js +437 -0
  16. package/dist/core/{approval → document}/forbidden-patterns.d.ts +4 -2
  17. package/dist/core/document/forbidden-patterns.js +36 -0
  18. package/dist/core/document/gates.d.ts +39 -0
  19. package/dist/core/document/gates.js +75 -0
  20. package/dist/core/document/input-digest.d.ts +63 -0
  21. package/dist/core/document/input-digest.js +200 -0
  22. package/dist/core/document/input-features.d.ts +43 -0
  23. package/dist/core/document/input-features.js +161 -0
  24. package/dist/core/document/lint.d.ts +39 -0
  25. package/dist/core/document/lint.js +147 -0
  26. package/dist/core/document/llm.d.ts +28 -0
  27. package/dist/core/document/llm.js +22 -0
  28. package/dist/core/document/map.d.ts +52 -0
  29. package/dist/core/document/map.js +296 -0
  30. package/dist/core/document/outline.d.ts +29 -0
  31. package/dist/core/document/outline.js +171 -0
  32. package/dist/core/document/paths.d.ts +23 -0
  33. package/dist/core/document/paths.js +36 -0
  34. package/dist/core/document/profile-validator.d.ts +23 -0
  35. package/dist/core/document/profile-validator.js +155 -0
  36. package/dist/core/document/profiles.d.ts +23 -0
  37. package/dist/core/document/profiles.js +74 -0
  38. package/dist/core/document/render.d.ts +45 -0
  39. package/dist/core/document/render.js +193 -0
  40. package/dist/core/document/review.d.ts +57 -0
  41. package/dist/core/document/review.js +202 -0
  42. package/dist/core/document/scene-detect.d.ts +32 -0
  43. package/dist/core/document/scene-detect.js +237 -0
  44. package/dist/core/document/schemas.d.ts +1103 -0
  45. package/dist/core/document/schemas.js +171 -0
  46. package/dist/integrations/shared/capability-evidence.js +4 -9
  47. package/dist/integrations/shared/command-catalog.js +2 -1
  48. package/dist/integrations/shared/parity-manifest.js +4 -9
  49. package/package.json +2 -1
  50. package/prompts/apply/phase-a-plan.md +1 -1
  51. package/prompts/document/map/acceptance.md +9 -0
  52. package/prompts/document/map/anti-ai.md +29 -0
  53. package/prompts/document/map/api-design.md +26 -0
  54. package/prompts/document/map/architecture.md +27 -0
  55. package/prompts/document/map/benchmark.md +26 -0
  56. package/prompts/document/map/closed-loop.md +9 -0
  57. package/prompts/document/map/compat-migration.md +31 -0
  58. package/prompts/document/map/component-design.md +30 -0
  59. package/prompts/document/map/config-runtime.md +9 -0
  60. package/prompts/document/map/core-flow.md +62 -0
  61. package/prompts/document/map/core-logic.md +9 -0
  62. package/prompts/document/map/data-model.md +18 -0
  63. package/prompts/document/map/deploy.md +26 -0
  64. package/prompts/document/map/fix.md +8 -0
  65. package/prompts/document/map/frontend-architecture.md +35 -0
  66. package/prompts/document/map/goal.md +8 -0
  67. package/prompts/document/map/impact.md +8 -0
  68. package/prompts/document/map/implementability.md +9 -0
  69. package/prompts/document/map/migration-guide.md +36 -0
  70. package/prompts/document/map/mvp-boundary.md +8 -0
  71. package/prompts/document/map/non-goals.md +8 -0
  72. package/prompts/document/map/ops.md +33 -0
  73. package/prompts/document/map/performance.md +32 -0
  74. package/prompts/document/map/poc-demo.md +25 -0
  75. package/prompts/document/map/regression.md +8 -0
  76. package/prompts/document/map/reproduce.md +8 -0
  77. package/prompts/document/map/requirement.md +8 -0
  78. package/prompts/document/map/research.md +25 -0
  79. package/prompts/document/map/root-cause.md +8 -0
  80. package/prompts/document/map/signoff.md +9 -0
  81. package/prompts/document/map/state-management.md +23 -0
  82. package/prompts/document/map/tech-selection.md +29 -0
  83. package/prompts/document/map/test-strategy.md +24 -0
  84. package/prompts/document/map/ui-design.md +21 -0
  85. package/prompts/document/outline/general.md +46 -0
  86. package/prompts/document/review/ai-review.md +57 -0
  87. package/prompts/document/shared/grounding.md +84 -0
  88. package/prompts/explore/explore-session.md +1 -1
  89. package/prompts/propose/design-draft.md +1 -1
  90. package/prompts/propose/proposal.md +1 -1
  91. package/prompts/propose/specs.md +1 -1
  92. package/prompts/propose/tasks-draft.md +1 -1
  93. package/prompts/refine/design-output.md +1 -1
  94. package/prompts/refine/update-artifacts.md +2 -0
  95. package/prompts/shared/artifact-language.md +13 -0
  96. package/skills/specflow-techdoc/SKILL.md +143 -0
  97. package/skills/specflow-techdoc-synth/SKILL.md +99 -0
  98. package/templates/document/chapters/acceptance.yaml +16 -0
  99. package/templates/document/chapters/api-design.yaml +29 -0
  100. package/templates/document/chapters/architecture.yaml +21 -0
  101. package/templates/document/chapters/benchmark.yaml +20 -0
  102. package/templates/document/chapters/closed-loop.yaml +22 -0
  103. package/templates/document/chapters/compat-migration.yaml +21 -0
  104. package/templates/document/chapters/component-design.yaml +22 -0
  105. package/templates/document/chapters/config-runtime.yaml +16 -0
  106. package/templates/document/chapters/core-flow.yaml +27 -0
  107. package/templates/document/chapters/core-logic.yaml +16 -0
  108. package/templates/document/chapters/data-model.yaml +25 -0
  109. package/templates/document/chapters/deploy.yaml +22 -0
  110. package/templates/document/chapters/fix.yaml +18 -0
  111. package/templates/document/chapters/frontend-architecture.yaml +22 -0
  112. package/templates/document/chapters/goal.yaml +17 -0
  113. package/templates/document/chapters/impact.yaml +16 -0
  114. package/templates/document/chapters/implementability.yaml +22 -0
  115. package/templates/document/chapters/migration-guide.yaml +21 -0
  116. package/templates/document/chapters/mvp-boundary.yaml +20 -0
  117. package/templates/document/chapters/non-goals.yaml +17 -0
  118. package/templates/document/chapters/ops.yaml +25 -0
  119. package/templates/document/chapters/performance.yaml +21 -0
  120. package/templates/document/chapters/poc-demo.yaml +22 -0
  121. package/templates/document/chapters/regression.yaml +16 -0
  122. package/templates/document/chapters/reproduce.yaml +16 -0
  123. package/templates/document/chapters/requirement.yaml +18 -0
  124. package/templates/document/chapters/research.yaml +22 -0
  125. package/templates/document/chapters/root-cause.yaml +18 -0
  126. package/templates/document/chapters/signoff.yaml +15 -0
  127. package/templates/document/chapters/state-management.yaml +22 -0
  128. package/templates/document/chapters/tech-selection.yaml +23 -0
  129. package/templates/document/chapters/test-strategy.yaml +23 -0
  130. package/templates/document/chapters/ui-design.yaml +28 -0
  131. package/templates/document/profiles/0to1.yaml +44 -0
  132. package/templates/document/profiles/approve.yaml +22 -0
  133. package/templates/document/profiles/bugfix.yaml +15 -0
  134. package/templates/document/profiles/feature.yaml +23 -0
  135. package/templates/document/profiles/frontend-0to1.yaml +47 -0
  136. package/templates/document/profiles/migration.yaml +42 -0
  137. package/templates/document/profiles/poc.yaml +46 -0
  138. package/dist/cli/commands/approval-assemble.d.ts +0 -64
  139. package/dist/cli/commands/approval-assemble.js +0 -408
  140. package/dist/core/approval/assemble.d.ts +0 -10
  141. package/dist/core/approval/assemble.js +0 -384
  142. package/dist/core/approval/bundle.d.ts +0 -9
  143. package/dist/core/approval/bundle.js +0 -172
  144. package/dist/core/approval/forbidden-patterns.js +0 -40
  145. package/dist/core/approval/index-schema.d.ts +0 -676
  146. package/dist/core/approval/index-schema.js +0 -193
  147. package/dist/core/approval/index.d.ts +0 -12
  148. package/dist/core/approval/index.js +0 -9
  149. package/dist/core/approval/lint.d.ts +0 -10
  150. package/dist/core/approval/lint.js +0 -302
  151. package/dist/core/approval/paths.d.ts +0 -13
  152. package/dist/core/approval/paths.js +0 -43
  153. package/dist/core/approval/pipeline.d.ts +0 -28
  154. package/dist/core/approval/pipeline.js +0 -146
  155. package/dist/core/approval/playbook-schema.d.ts +0 -182
  156. package/dist/core/approval/playbook-schema.js +0 -51
  157. package/dist/core/approval/render.d.ts +0 -20
  158. package/dist/core/approval/render.js +0 -210
  159. package/dist/core/approval/review-pack.d.ts +0 -26
  160. package/dist/core/approval/review-pack.js +0 -205
  161. package/dist/core/approval/types.d.ts +0 -223
  162. package/dist/core/approval/types.js +0 -1
  163. package/prompts/approval/acp-pipeline.md +0 -106
  164. package/prompts/approval/ai-review.md +0 -145
  165. package/prompts/approval/api-guidance.md +0 -179
  166. package/prompts/approval/database-guidance.md +0 -105
  167. package/prompts/approval/frontend-guidance.md +0 -249
  168. package/prompts/approval/generate.md +0 -1596
  169. package/prompts/approval/multi-repo-guidance.md +0 -263
  170. package/prompts/approval/multi-repo-spoke-subagent.md +0 -94
  171. package/prompts/approval/project-conventions-guidance.md +0 -171
  172. package/prompts/approval/runtime-guidance.md +0 -64
  173. package/prompts/approval/segmented-generation.md +0 -157
  174. package/skills/specflow-approval/SKILL.md +0 -778
  175. package/templates/approval-index.yaml +0 -93
  176. package/templates/approval-part.md +0 -15
  177. package/templates/approval-playbook-talos-scenario-job-compile.yaml +0 -29
  178. package/templates/approval-playbook.yaml +0 -28
  179. package/templates/approval.md +0 -261
@@ -0,0 +1,845 @@
1
+ /**
2
+ * CLI: specflow techdoc {plan|validate|run} — Generic LongDoc Engine, IDE-model driven.
3
+ *
4
+ * The engine does NOT call a third-party LLM API. In Cursor/Codex the Agent runs on the IDE's
5
+ * model, so:
6
+ * - `document plan <profile>` emits a step list the Agent executes with the IDE model.
7
+ * - `document validate --work-root <path>` deterministically validates + renders Agent-written
8
+ * artifacts (outline.yaml / chapters/*.md / entities.json) — no LLM call.
9
+ * - `document run <profile>` guides: plan → Agent executes → validate (requires injected LLM only
10
+ * for library/tests; CLI itself never fetches an API).
11
+ */
12
+ import { promises as fs } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { loadChapterLibrary } from '../../core/document/chapters.js';
15
+ import { loadProfile } from '../../core/document/profiles.js';
16
+ import { runDocument, planSteps, validateWork } from '../../core/document/engine.js';
17
+ import { detectInputFeatures } from '../../core/document/input-features.js';
18
+ import { detectScene, confirmationPrompt } from '../../core/document/scene-detect.js';
19
+ import { reposPath } from '../../core/document/paths.js';
20
+ import { readOutline } from '../../core/document/outline.js';
21
+ async function readInputFile(path, type) {
22
+ const content = await fs.readFile(path, 'utf-8');
23
+ return { type, source: path, content };
24
+ }
25
+ /**
26
+ * 解析 --max-rounds(P2):commander 以字符串传入,'abc'/'0' 会静默旁路 Pass 3 审查。
27
+ * 非法或 <1 → 抛错(由命令顶层 catch 转为 exit 1),避免质量门禁被悄悄跳过。
28
+ */
29
+ function parseMaxRounds(value) {
30
+ if (value === undefined)
31
+ return 3;
32
+ const n = typeof value === 'number' ? value : Number(value);
33
+ if (!Number.isInteger(n) || n < 1) {
34
+ throw new Error(`Invalid --max-rounds "${value}": must be a positive integer`);
35
+ }
36
+ return n;
37
+ }
38
+ async function collectInputs(options) {
39
+ const inputs = [];
40
+ if (options.text)
41
+ inputs.push({ type: 'description', source: 'description', content: options.text });
42
+ if (options.proposal)
43
+ inputs.push(await readInputFile(options.proposal, 'proposal'));
44
+ if (options.design)
45
+ inputs.push(await readInputFile(options.design, 'design'));
46
+ if (options.specs)
47
+ inputs.push(await readInputFile(options.specs, 'specs'));
48
+ if (options.tasks)
49
+ inputs.push(await readInputFile(options.tasks, 'tasks'));
50
+ if (options.code)
51
+ inputs.push(await readInputFile(options.code, 'code'));
52
+ return inputs;
53
+ }
54
+ function inputFeatures(inputs, opts) {
55
+ // Content-based detection (deterministic keyword scoring), plus the explicit --signoff flag.
56
+ // Previously `uiInScope` was hardcoded false and `hasLogic` always true here, which made
57
+ // `ui-design` unreachable and skipped conditional chapters regardless of the input.
58
+ return {
59
+ ...detectInputFeatures(inputs),
60
+ includeSignoff: opts?.includeSignoff ?? false,
61
+ };
62
+ }
63
+ function stepsToMarkdown(steps, profileId) {
64
+ const lines = [
65
+ `# 文档生成步骤(${profileId})`,
66
+ '',
67
+ '> 请你在当前 IDE 会话中用模型逐步执行以下步骤,产物写入 workRoot。',
68
+ '> 完成后运行 `specflow techdoc validate --work-root <path>` 校验。',
69
+ '',
70
+ ];
71
+ for (const s of steps) {
72
+ lines.push(`## ${s.id}(${s.phase})`, '');
73
+ lines.push(`**做什么**:${s.description}`, '');
74
+ lines.push(`**参考提示词**:\`${s.prompt}\``, '');
75
+ if (s.inputFiles.length)
76
+ lines.push(`**输入**:${s.inputFiles.join(', ')}`, '');
77
+ lines.push(`**输出**:\`${s.outputFile}\``, '');
78
+ lines.push(`**必做**:${s.required ? '是' : '否(可选)'}`, '');
79
+ lines.push('', '---', '');
80
+ }
81
+ return lines.join('\n');
82
+ }
83
+ // ============= document plan =============
84
+ export async function planDocumentCommand(profileId, options) {
85
+ try {
86
+ const library = await loadChapterLibrary();
87
+ const profile = await loadProfile(profileId);
88
+ const inputs = await collectInputs(options);
89
+ const workRoot = options.workRoot ?? join(process.cwd(), '.specflow', 'document');
90
+ await fs.mkdir(workRoot, { recursive: true });
91
+ const result = planSteps({
92
+ profile,
93
+ library,
94
+ inputFeatures: inputFeatures(inputs, { includeSignoff: options.signoff }),
95
+ inputs,
96
+ workRoot,
97
+ projectRoot: process.cwd(),
98
+ });
99
+ const payload = {
100
+ ok: true,
101
+ profile: profileId,
102
+ workRoot,
103
+ chapters: result.chapters,
104
+ steps: result.steps,
105
+ };
106
+ if (options.json) {
107
+ return { exitCode: 0, payload };
108
+ }
109
+ if (options.markdown) {
110
+ console.info(stepsToMarkdown(result.steps, profileId));
111
+ return { exitCode: 0, payload };
112
+ }
113
+ console.info(`文档生成步骤(${profileId}),workRoot: ${workRoot}`);
114
+ console.info('');
115
+ for (const s of result.steps) {
116
+ console.info(` [${s.phase}] ${s.id}: ${s.title}`);
117
+ console.info(` → ${s.outputFile}`);
118
+ }
119
+ console.info('');
120
+ console.info('请用 IDE 模型按上述步骤生成产物,然后运行:');
121
+ console.info(` specflow techdoc validate --work-root ${workRoot}`);
122
+ return { exitCode: 0, payload };
123
+ }
124
+ catch (error) {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ if (options.json)
127
+ return { exitCode: 1, payload: { ok: false, error: message } };
128
+ console.error(`Error: ${message}`);
129
+ return { exitCode: 1, payload: { ok: false, error: message } };
130
+ }
131
+ }
132
+ // ============= document validate =============
133
+ export async function validateDocumentCommand(options) {
134
+ try {
135
+ const workRoot = options.workRoot ?? join(process.cwd(), '.specflow', 'document');
136
+ const library = await loadChapterLibrary();
137
+ // Detect profile from outline.yaml (or require it). 复用 readOutline 的 YAML/JSON 双格式
138
+ // 解析(P2):手写正则探测对 JSON 格式/带引号 profile 会误判。
139
+ const outline = await readOutline(workRoot);
140
+ const profile = await loadProfile(outline.profile);
141
+ const result = await validateWork({
142
+ profile,
143
+ library,
144
+ workRoot,
145
+ renderHtmlOutput: !options.noRender,
146
+ });
147
+ const payload = {
148
+ ok: result.ok,
149
+ workRoot,
150
+ document: result.documentMd ? join(workRoot, 'document.md') : undefined,
151
+ html: result.documentHtml ? join(workRoot, 'document.html') : undefined,
152
+ diagnostics: result.diagnostics,
153
+ };
154
+ if (options.json) {
155
+ return { exitCode: result.ok ? 0 : 1, payload };
156
+ }
157
+ if (!result.ok) {
158
+ console.error(`Document validation failed:`);
159
+ for (const d of result.diagnostics) {
160
+ console.error(` [${d.gate}][${d.severity}] ${d.message}`);
161
+ }
162
+ return { exitCode: 1, payload };
163
+ }
164
+ console.info(`Document valid: ${join(workRoot, 'document.md')}`);
165
+ if (result.documentHtml)
166
+ console.info(`HTML: ${join(workRoot, 'document.html')}`);
167
+ return { exitCode: 0, payload };
168
+ }
169
+ catch (error) {
170
+ const message = error instanceof Error ? error.message : String(error);
171
+ if (options.json)
172
+ return { exitCode: 1, payload: { ok: false, error: message } };
173
+ console.error(`Error: ${message}`);
174
+ return { exitCode: 1, payload: { ok: false, error: message } };
175
+ }
176
+ }
177
+ // ============= document run (Agent-driven guide; library `runDocument` needs an injected LLM) =============
178
+ let injectedLLM = null;
179
+ export function injectLLM(llm) {
180
+ injectedLLM = llm;
181
+ }
182
+ function getInjectedLLM() {
183
+ return injectedLLM;
184
+ }
185
+ export async function runDocumentCommand(profileId, options) {
186
+ try {
187
+ // Collect inputs first (needed for scene auto-detection).
188
+ const inputs = await collectInputs(options);
189
+ if (inputs.length === 0) {
190
+ const msg = 'No input provided. Use --text or --proposal/--design/--specs/--code.';
191
+ if (options.json)
192
+ return { exitCode: 1, payload: { ok: false, error: msg } };
193
+ console.error(`Error: ${msg}`);
194
+ return { exitCode: 1, payload: { ok: false, error: msg } };
195
+ }
196
+ const text = inputs.map((i) => i.content).join('\n');
197
+ // Scene resolution: explicit profile, or auto-detect from natural language; confirm when uncertain.
198
+ let resolvedProfile = profileId ?? '';
199
+ if (!resolvedProfile) {
200
+ const det = detectScene(text);
201
+ if (det.profile && det.confidence >= 0.5) {
202
+ resolvedProfile = det.profile;
203
+ }
204
+ else {
205
+ const msg = confirmationPrompt(det, text) +
206
+ '\n\n确认后重新运行,例如:\n specflow techdoc run <profile> --text "..."';
207
+ const payload = { ok: false, needsConfirmation: true, candidates: det.candidates, message: msg };
208
+ if (options.json)
209
+ return { exitCode: 0, payload };
210
+ console.info(msg);
211
+ return { exitCode: 0, payload: { ok: false, needsConfirmation: true } };
212
+ }
213
+ }
214
+ // One-click entry: no injected LLM → emit the Agent execution directive. The Agent (running on
215
+ // the IDE model) reads this and auto-completes plan → generate → validate. CLI itself never fetches an API.
216
+ const llm = getInjectedLLM();
217
+ if (!llm) {
218
+ const workRoot = options.workRoot ?? join(process.cwd(), '.specflow', 'document');
219
+ await fs.mkdir(workRoot, { recursive: true });
220
+ const library = await loadChapterLibrary();
221
+ const profile = await loadProfile(resolvedProfile);
222
+ const plan = planSteps({ profile, library, inputFeatures: inputFeatures(inputs, { includeSignoff: options.signoff }), inputs, workRoot, projectRoot: process.cwd() });
223
+ const directive = [
224
+ `# 一键生成技术文档(${resolvedProfile})`,
225
+ '',
226
+ `请作为 Agent 用当前 IDE 模型自动完成以下步骤(勿中途停下来等用户分步),产物写入 workRoot: ${workRoot}`,
227
+ '',
228
+ `## 步骤`,
229
+ ...plan.steps.map((s) => `- [${s.phase}] **${s.id}**: ${s.title} → 输出 \`${s.outputFile}\`${s.required ? '' : '(可选)'}`),
230
+ '',
231
+ `## 完成后`,
232
+ `运行 \`specflow techdoc validate --work-root ${workRoot}\`,通过后向用户报告 document.md 路径;失败则修正产物后重跑。`,
233
+ '',
234
+ `提示词模板参考(IDE 资产):`,
235
+ `- outline: .claude/specflow/prompts/document/outline/general.md`,
236
+ `- map: .claude/specflow/prompts/document/map/<章节>.md`,
237
+ `- review: .claude/specflow/prompts/document/review/ai-review.md`,
238
+ ].join('\n');
239
+ const payload = { ok: true, mode: 'agent-directive', workRoot, steps: plan.steps };
240
+ if (options.json)
241
+ return { exitCode: 0, payload };
242
+ console.info(directive);
243
+ return { exitCode: 0, payload };
244
+ }
245
+ // Delegates to the library runDocument for completeness (tests use this).
246
+ try {
247
+ const library = await loadChapterLibrary();
248
+ const profile = await loadProfile(resolvedProfile);
249
+ const workRoot = options.workRoot ?? join(process.cwd(), '.specflow', 'document');
250
+ await fs.mkdir(workRoot, { recursive: true });
251
+ const result = await runDocument({
252
+ llm,
253
+ workRoot,
254
+ profile,
255
+ library,
256
+ inputs,
257
+ inputFeatures: inputFeatures(inputs, { includeSignoff: options.signoff }),
258
+ change: resolvedProfile,
259
+ skipReview: options.skipReview ?? false,
260
+ maxRounds: parseMaxRounds(options.maxRounds),
261
+ renderHtmlOutput: false,
262
+ persist: true,
263
+ projectRoot: process.cwd(),
264
+ });
265
+ const payload = {
266
+ ok: result.ok,
267
+ stage: result.stage,
268
+ workRoot,
269
+ document: result.documentMd ? join(workRoot, 'document.md') : undefined,
270
+ diagnostics: result.diagnostics,
271
+ };
272
+ if (options.json)
273
+ return { exitCode: result.ok ? 0 : 1, payload };
274
+ if (!result.ok) {
275
+ console.error(`Document generation failed at stage "${result.stage}":`);
276
+ for (const d of result.diagnostics)
277
+ console.error(` [${d.gate}][${d.severity}] ${d.message}`);
278
+ return { exitCode: 1, payload };
279
+ }
280
+ console.info(`Document generated (${resolvedProfile}) in ${workRoot}`);
281
+ return { exitCode: 0, payload };
282
+ }
283
+ catch (error) {
284
+ const message = error instanceof Error ? error.message : String(error);
285
+ if (options.json)
286
+ return { exitCode: 1, payload: { ok: false, error: message } };
287
+ console.error(`Error: ${message}`);
288
+ return { exitCode: 1, payload: { ok: false, error: message } };
289
+ }
290
+ }
291
+ catch (error) {
292
+ // 顶层兜底:collectInputs(文件不存在)/ loadProfile / planSteps / 场景识别等
293
+ // 任一失败都不再以未捕获 rejection 崩溃,--json 契约始终成立(P0-4)。
294
+ const message = error instanceof Error ? error.message : String(error);
295
+ if (options.json)
296
+ return { exitCode: 1, payload: { ok: false, error: message } };
297
+ console.error(`Error: ${message}`);
298
+ return { exitCode: 1, payload: { ok: false, error: message } };
299
+ }
300
+ }
301
+ // ============= document detect =============
302
+ export async function detectDocumentCommand(options) {
303
+ try {
304
+ const inputs = await collectInputs(options);
305
+ const text = inputs.map((i) => i.content).join('\n');
306
+ const det = detectScene(text);
307
+ const payload = {
308
+ ok: true,
309
+ detected: det.profile,
310
+ confidence: det.confidence,
311
+ candidates: det.candidates,
312
+ reasons: det.reasons,
313
+ };
314
+ if (options.json)
315
+ return { exitCode: 0, payload };
316
+ if (det.profile) {
317
+ console.info(`检测到场景: ${det.profile}(置信度 ${Math.round(det.confidence * 100)}%)`);
318
+ }
319
+ else {
320
+ console.info(confirmationPrompt(det, text));
321
+ }
322
+ return { exitCode: 0, payload };
323
+ }
324
+ catch (error) {
325
+ const message = error instanceof Error ? error.message : String(error);
326
+ if (options.json)
327
+ return { exitCode: 1, payload: { ok: false, error: message } };
328
+ console.error(`Error: ${message}`);
329
+ return { exitCode: 1, payload: { ok: false, error: message } };
330
+ }
331
+ }
332
+ /**
333
+ * Discover multi-repo four-artifact sets under a workspace root.
334
+ * A repo contributes if `<repoRoot>/specflow/changes/<change>/` contains proposal/specs/design/tasks.
335
+ */
336
+ async function discoverRepoChanges(workspaceRoot, spec) {
337
+ const repos = [];
338
+ if (spec) {
339
+ for (const part of spec.split(',')) {
340
+ const [repo, change] = part.trim().split(':');
341
+ if (repo && change)
342
+ repos.push({ repo, change, root: join(workspaceRoot, repo) });
343
+ }
344
+ return repos;
345
+ }
346
+ // Auto-scan: workspaceRoot/<repo>/specflow/changes/*/
347
+ const entries = await fs.readdir(workspaceRoot, { withFileTypes: true });
348
+ for (const e of entries) {
349
+ if (!e.isDirectory())
350
+ continue;
351
+ const changesDir = join(workspaceRoot, e.name, 'specflow', 'changes');
352
+ try {
353
+ const changes = await fs.readdir(changesDir);
354
+ for (const ch of changes) {
355
+ if (ch.startsWith('archive'))
356
+ continue;
357
+ repos.push({ repo: e.name, change: ch, root: join(workspaceRoot, e.name) });
358
+ }
359
+ }
360
+ catch {
361
+ // no specflow/changes in this dir → skip
362
+ }
363
+ }
364
+ return repos;
365
+ }
366
+ export async function approveDocumentCommand(options) {
367
+ try {
368
+ const library = await loadChapterLibrary();
369
+ const profile = await loadProfile('approve');
370
+ const dirs = [];
371
+ if (options.workspaceRoot) {
372
+ dirs.push(...await discoverRepoChanges(options.workspaceRoot, options.changes ?? ''));
373
+ }
374
+ else {
375
+ const change = options.change ?? 'approval';
376
+ dirs.push({ repo: 'local', change, root: process.cwd() });
377
+ }
378
+ if (dirs.length === 0) {
379
+ const msg = 'No four-artifact change found. Provide --proposal/--specs/--design/--tasks or --workspace-root.';
380
+ if (options.json)
381
+ return { exitCode: 1, payload: { ok: false, error: msg } };
382
+ console.error(`Error: ${msg}`);
383
+ return { exitCode: 1, payload: { ok: false, error: msg } };
384
+ }
385
+ // Per-repo approve plan (read four artifacts, emit Agent generation steps).
386
+ const plans = [];
387
+ for (const d of dirs) {
388
+ const changeDir = join(d.root, 'specflow', 'changes', d.change);
389
+ const inputs = [];
390
+ for (const [type, file] of [['proposal', 'proposal.md'], ['specs', 'spec.md'], ['design', 'design.md'], ['tasks', 'tasks.md']]) {
391
+ try {
392
+ const content = await fs.readFile(join(changeDir, file), 'utf-8');
393
+ inputs.push({ type, source: join(changeDir, file), content });
394
+ }
395
+ catch {
396
+ // optional missing artifact → skip
397
+ }
398
+ }
399
+ if (inputs.length === 0) {
400
+ // fall back to explicit options (four artifacts)
401
+ if (options.proposal)
402
+ inputs.push(await readInputFile(options.proposal, 'proposal'));
403
+ if (options.specs)
404
+ inputs.push(await readInputFile(options.specs, 'specs'));
405
+ if (options.design)
406
+ inputs.push(await readInputFile(options.design, 'design'));
407
+ if (options.tasks)
408
+ inputs.push(await readInputFile(options.tasks, 'tasks'));
409
+ }
410
+ if (inputs.length === 0) {
411
+ // 空输入拦截(P2):该仓四件套缺失且无显式选项时禁止基于零输入生成。
412
+ const msg = `Repo ${d.repo}/${d.change}: no four-artifact inputs found. Provide --proposal/--specs/--design/--tasks or ensure artifacts exist.`;
413
+ if (options.json)
414
+ return { exitCode: 1, payload: { ok: false, error: msg } };
415
+ console.error(`Error: ${msg}`);
416
+ return { exitCode: 1, payload: { ok: false, error: msg } };
417
+ }
418
+ // 多仓模式:显式 --work-root 下按 repo 派生子目录,避免各仓产物互相覆盖(P2)。
419
+ const workRoot = options.workRoot
420
+ ? join(options.workRoot, d.repo)
421
+ : join(changeDir, 'approval');
422
+ await fs.mkdir(workRoot, { recursive: true });
423
+ const plan = planSteps({
424
+ profile,
425
+ library,
426
+ inputFeatures: inputFeatures(inputs, { includeSignoff: options.signoff }),
427
+ inputs,
428
+ workRoot,
429
+ projectRoot: d.root,
430
+ });
431
+ plans.push({ repo: d.repo, change: d.change, steps: plan.steps, workRoot });
432
+ }
433
+ // Multi-repo bundle: merge each repo's generated document.md into one approval-unified.md.
434
+ if (options.bundle) {
435
+ if (plans.length < 2) {
436
+ const msg = '--bundle requires multiple repos (--workspace-root or --changes with ≥2).';
437
+ if (options.json)
438
+ return { exitCode: 1, payload: { ok: false, error: msg } };
439
+ console.error(`Error: ${msg}`);
440
+ return { exitCode: 1, payload: { ok: false, error: msg } };
441
+ }
442
+ const sections = [`# 审批合订(多仓)`, '', `> 各仓审批文档合订视图(只读)。各仓 document.md 为真源。`, ''];
443
+ const errors = [];
444
+ for (const p of plans) {
445
+ const docPath = join(p.workRoot, 'document.md');
446
+ try {
447
+ const content = await fs.readFile(docPath, 'utf-8');
448
+ sections.push(`---`, '', `## 来源 · ${p.repo}(\`${p.change}\`)`, '', content.trim(), '');
449
+ }
450
+ catch {
451
+ errors.push(`${p.repo}/${p.change} document.md 未生成(请先完成该仓生成并 validate)`);
452
+ }
453
+ }
454
+ if (errors.length > 0) {
455
+ const msg = `合订失败:\n ${errors.join('\n ')}`;
456
+ if (options.json)
457
+ return { exitCode: 1, payload: { ok: false, error: msg } };
458
+ console.error(msg);
459
+ return { exitCode: 1, payload: { ok: false, error: msg } };
460
+ }
461
+ const unifiedPath = join(plans[0].workRoot, 'approval-unified.md');
462
+ await fs.writeFile(unifiedPath, sections.join('\n'), 'utf-8');
463
+ const payload = { ok: true, mode: 'bundle', unifiedPath, sources: plans.map((p) => ({ repo: p.repo, change: p.change, document: join(p.workRoot, 'document.md') })) };
464
+ if (options.json)
465
+ return { exitCode: 0, payload };
466
+ console.info(`多仓合订完成: ${unifiedPath}`);
467
+ return { exitCode: 0, payload };
468
+ }
469
+ // Build directive.
470
+ const lines = ['# 审批文档生成(approve · 四件套 → 审批)', ''];
471
+ lines.push('请作为 Agent 用当前 IDE 模型,对每个仓的四件套生成审批文档(含闭环检查/可实施性/质量红线),产物写入各仓 workRoot,完成后 validate。');
472
+ lines.push('');
473
+ for (const p of plans) {
474
+ lines.push(`## 仓 ${p.repo}(change: ${p.change})· workRoot: ${p.workRoot}`);
475
+ for (const s of p.steps) {
476
+ lines.push(`- [${s.phase}] **${s.id}**: ${s.title} → \`${s.outputFile}\``);
477
+ }
478
+ lines.push(` - 完成后: \`specflow techdoc validate --work-root ${p.workRoot}\``);
479
+ lines.push('');
480
+ }
481
+ if (plans.length > 1) {
482
+ lines.push('## 多仓合订(主仓)');
483
+ lines.push('各仓审批文档生成并 validate 通过后,主仓汇总为统一视图(只读合订):');
484
+ lines.push(` - 合并各仓 document.md 为 approval-unified.md(主仓: ${plans[0].repo})`);
485
+ }
486
+ const payload = { ok: true, mode: 'approve', plans: plans.map((p) => ({ repo: p.repo, change: p.change, workRoot: p.workRoot, steps: p.steps })) };
487
+ if (options.json)
488
+ return { exitCode: 0, payload };
489
+ console.info(lines.join('\n'));
490
+ return { exitCode: 0, payload };
491
+ }
492
+ catch (error) {
493
+ const message = error instanceof Error ? error.message : String(error);
494
+ if (options.json)
495
+ return { exitCode: 1, payload: { ok: false, error: message } };
496
+ console.error(`Error: ${message}`);
497
+ return { exitCode: 1, payload: { ok: false, error: message } };
498
+ }
499
+ }
500
+ /**
501
+ * 读取一个仓的四件套,返回带来源标记 + repo 字段的输入。
502
+ * - content 前置 `## 来源仓:<repo>(change: <change>)`:压缩器/大纲/章节填充可见归属。
503
+ * - `repo` 字段:段 id 携带仓前缀(`seg-repo1-proposal-01`),Pass 2 按需取回对应仓原文。
504
+ */
505
+ async function collectRepoInputs(d) {
506
+ const changeDir = join(d.root, 'specflow', 'changes', d.change);
507
+ const marker = `## 来源仓:${d.repo}(change: ${d.change})`;
508
+ const inputs = [];
509
+ for (const [type, file] of [['proposal', 'proposal.md'], ['specs', 'spec.md'], ['design', 'design.md'], ['tasks', 'tasks.md']]) {
510
+ try {
511
+ const content = await fs.readFile(join(changeDir, file), 'utf-8');
512
+ inputs.push({ type, source: join(changeDir, file), content: `${marker}\n\n${content}`, repo: d.repo });
513
+ }
514
+ catch {
515
+ // optional missing artifact → skip
516
+ }
517
+ }
518
+ return inputs;
519
+ }
520
+ /**
521
+ * 跨仓合成规则(directive 与库形态 outlinePrompt 共用同一套文本)。
522
+ * 规则 6(按需加载规约):写某仓章节时只读取该仓的规约/IDE rules,禁止一次性读全部仓——
523
+ * 防止 token 暴涨 + 注意力稀释;仅写跨仓内容(依赖/契约对接)时才读取涉及仓的规约。
524
+ */
525
+ function buildSynthRules(dirs) {
526
+ const repoList = dirs.map((d) => d.repo).join('、');
527
+ return [
528
+ `## 跨仓合成规则(必须遵守)`,
529
+ `1. **统一大纲**:一次生成一份 outline,章节覆盖所有仓,不按仓分章(跨仓对比/汇总放进对应章节)。`,
530
+ `2. **契约实体命名空间**:跨仓实体 id 用 \`<repo>_<id>\` 前缀(如 \`repo1_I1\`、\`repo2_T1\`),禁止不同仓共用裸 id。`,
531
+ `3. **每章必须分仓小节(质量门禁,validate 强制)**:每个必选章节的叙述按仓分节,用 \`### <repo>\` 小节标题(可带后缀如 \`### repo1 · 目标\`),且每个仓(${repoList})都必须出现。禁止把多仓内容混写成一段(会导致细节互相稀释)。`,
532
+ `4. **全局视角**(若所选 profile 包含对应章节):goal 写多仓总目标与边界;tech-selection 对比各仓技术方案;api-design/data-model 分仓列契约;closed-loop 跨仓统一闭环检查;implementability 跨仓统一可实施性评估。其他场景章节(如 reproduce/root-cause/fix、requirement/test-strategy、research/poc-demo/benchmark)同样按仓分节并给出跨仓结论。`,
533
+ `5. **跨仓依赖/风险**:涉及仓间依赖(接口调用/数据共享/发布顺序)时显式标注「跨仓依赖」。`,
534
+ `6. **按需加载规约(重要)**:写某个仓的小节时,**只读取该仓**的规约/IDE rules/guidance(该仓的 \`docs/\`、\`.cursor\`、\`.claude\`、\`.agents\` 等),**禁止一次性读取全部仓的规约**;只有写跨仓内容(接口对接/数据共享/发布顺序)时才读取**涉及仓**的规约。未读取的仓规约不得臆测,必要时标注「未读取该仓规约,待确认」。`,
535
+ `7. **输入归属**:每段输入带「来源仓」标记,段 id 携带仓前缀(如 \`seg-repo1-proposal-01\`);引用原文时按段 id 精确取回对应仓的片段,不整篇重读。`,
536
+ ].join('\n');
537
+ }
538
+ /**
539
+ * 解析合成用 profile:显式 --profile 优先;省略时从 --text(或四件套拼接内容)做场景识别。
540
+ * - 识别置信度达标 → 直接采用识别结果。
541
+ * - 识别不出且用户给了 --text → 返回 needsConfirmation(与 document run 行为一致,向用户确认)。
542
+ * - 识别不出且无 --text → 回退 approve(原 synthesize 默认,保持向后兼容,不打断流程)。
543
+ */
544
+ function resolveSynthProfile(options, inputs) {
545
+ if (options.profile)
546
+ return { profileId: options.profile };
547
+ const detectText = (options.text ?? inputs.map((i) => i.content).join('\n')).trim();
548
+ const det = detectScene(detectText);
549
+ if (det.profile && det.confidence >= 0.5) {
550
+ return { profileId: det.profile, detectedText: detectText };
551
+ }
552
+ if (options.text) {
553
+ const message = confirmationPrompt(det, detectText) +
554
+ '\n\n确认后重新运行,例如:\n specflow techdoc synthesize --workspace-root <root> --profile <profile> ...';
555
+ return {
556
+ profileId: null,
557
+ detectedText: detectText,
558
+ needsConfirmation: { candidates: det.candidates, message },
559
+ };
560
+ }
561
+ // 无 --text:四件套无强场景信号 → 回退 approve(原默认),不打断流程。
562
+ return { profileId: 'approve', detectedText: detectText, fallback: true };
563
+ }
564
+ export async function synthesizeDocumentCommand(options) {
565
+ try {
566
+ // workspaceRoot 必填(P2):缺省时 fs.readdir(undefined) 会抛裸 TypeError。
567
+ if (!options.workspaceRoot) {
568
+ const msg = 'synthesize 需要 --workspace-root <path>(多仓工作区根目录,必填)。';
569
+ if (options.json)
570
+ return { exitCode: 1, payload: { ok: false, error: msg } };
571
+ console.error(`Error: ${msg}`);
572
+ return { exitCode: 1, payload: { ok: false, error: msg } };
573
+ }
574
+ const dirs = await discoverRepoChanges(options.workspaceRoot, options.changes ?? '');
575
+ if (dirs.length === 0) {
576
+ const msg = 'No multi-repo four-artifact sets found. Use --workspace-root with --changes "repo1:ch1,repo2:ch2" or auto-scan.';
577
+ if (options.json)
578
+ return { exitCode: 1, payload: { ok: false, error: msg } };
579
+ console.error(`Error: ${msg}`);
580
+ return { exitCode: 1, payload: { ok: false, error: msg } };
581
+ }
582
+ if (dirs.length < 2) {
583
+ const msg = 'synthesize 需要 ≥2 个仓(跨仓合成);单仓请用 document run。';
584
+ if (options.json)
585
+ return { exitCode: 1, payload: { ok: false, error: msg } };
586
+ console.error(`Error: ${msg}`);
587
+ return { exitCode: 1, payload: { ok: false, error: msg } };
588
+ }
589
+ // 统一输入集:可选 --text 描述 + 各仓四件套(带来源仓标记)。
590
+ const inputs = [];
591
+ if (options.text)
592
+ inputs.push({ type: 'description', source: 'description', content: options.text });
593
+ for (const d of dirs) {
594
+ inputs.push(...await collectRepoInputs(d));
595
+ }
596
+ // 空输入拦截(P2):四件套全缺且无 --text 时禁止基于零输入生成,与 run 命令行为一致。
597
+ if (inputs.length === 0) {
598
+ const msg = 'No input found: 各仓四件套(proposal/spec/design/tasks)均缺失,且未提供 --text。请先补齐四件套或提供 --text 描述。';
599
+ if (options.json)
600
+ return { exitCode: 1, payload: { ok: false, error: msg } };
601
+ console.error(`Error: ${msg}`);
602
+ return { exitCode: 1, payload: { ok: false, error: msg } };
603
+ }
604
+ // 场景识别:显式 --profile 优先;省略时从 --text / 四件套内容识别,不确定则向用户确认(同 document run)。
605
+ const resolved = resolveSynthProfile(options, inputs);
606
+ if (resolved.needsConfirmation) {
607
+ const payload = {
608
+ ok: false,
609
+ needsConfirmation: true,
610
+ candidates: resolved.needsConfirmation.candidates,
611
+ message: resolved.needsConfirmation.message,
612
+ };
613
+ if (options.json)
614
+ return { exitCode: 0, payload };
615
+ console.info(resolved.needsConfirmation.message);
616
+ return { exitCode: 0, payload };
617
+ }
618
+ const profileId = resolved.profileId;
619
+ const scene = options.profile
620
+ ? { profile: profileId, source: 'explicit' }
621
+ : resolved.fallback
622
+ ? { profile: profileId, source: 'fallback' }
623
+ : { profile: profileId, source: 'detected' };
624
+ const library = await loadChapterLibrary();
625
+ const profile = await loadProfile(profileId);
626
+ const workRoot = options.workRoot ?? join(options.workspaceRoot, '.specflow', 'document-synthesized');
627
+ await fs.mkdir(workRoot, { recursive: true });
628
+ // Multi-repo synthesize marker: validateWork reads this to enforce the per-repo section gate.
629
+ // 只有 synthesize 写 repos.json;approve --bundle / document run 不写 → 门禁不触发。
630
+ const repoNames = dirs.map((d) => d.repo);
631
+ await fs.writeFile(reposPath(workRoot), JSON.stringify(repoNames, null, 2), 'utf-8');
632
+ const llm = getInjectedLLM();
633
+ const synthRules = buildSynthRules(dirs);
634
+ if (!llm) {
635
+ // Agent-directive mode (CLI never fetches an LLM API — the Agent uses the IDE model).
636
+ // 不传 projectRoot=workspaceRoot:按需加载——规约由规则 6 要求按「章节涉及的仓」读取,
637
+ // 禁止一次性读取全部仓的规约/IDE rules(防止 token 暴涨 + 注意力稀释)。
638
+ const plan = planSteps({
639
+ profile,
640
+ library,
641
+ inputFeatures: inputFeatures(inputs, { includeSignoff: options.signoff }),
642
+ inputs,
643
+ workRoot,
644
+ });
645
+ const repoList = dirs.map((d) => `${d.repo}/${d.change}`).join('、');
646
+ const sceneLine = scene.source === 'detected'
647
+ ? `(场景识别:${scene.profile})`
648
+ : scene.source === 'fallback'
649
+ ? '(场景未识别,回退 approve)'
650
+ : '';
651
+ const directive = [
652
+ `# 跨仓合成文档(${profile.id} · ${dirs.length} 仓${sceneLine})`,
653
+ '',
654
+ `请作为 Agent 用当前 IDE 模型自动完成以下步骤(勿中途停下等用户分步),产物写入 workRoot: ${workRoot}`,
655
+ '',
656
+ `## 输入来源(多仓统一输入,每段已标记「来源仓」,段 id 携带仓前缀)`,
657
+ ...inputs.map((i) => `- \`${i.source}\`(${i.type} · 来自 ${i.content.split('\n')[0] ?? ''})`),
658
+ '',
659
+ synthRules,
660
+ '',
661
+ `## 步骤`,
662
+ ...plan.steps.map((s) => `- [${s.phase}] **${s.id}**: ${s.title} → 输出 \`${s.outputFile}\`${s.required ? '' : '(可选)'}`),
663
+ '',
664
+ `## 完成后`,
665
+ `运行 \`specflow techdoc validate --work-root ${workRoot}\`,通过后向用户报告 document.md 路径;失败则修正产物后重跑。`,
666
+ '',
667
+ `提示词模板参考(IDE 资产):`,
668
+ `- outline: .claude/specflow/prompts/document/outline/general.md`,
669
+ `- map: .claude/specflow/prompts/document/map/<章节>.md`,
670
+ `- review: .claude/specflow/prompts/document/review/ai-review.md`,
671
+ ].join('\n');
672
+ const payload = { ok: true, mode: 'synthesize-directive', profile: profile.id, scene, repos: repoList, workRoot, steps: plan.steps };
673
+ if (options.json)
674
+ return { exitCode: 0, payload };
675
+ console.info(directive);
676
+ return { exitCode: 0, payload };
677
+ }
678
+ // Library mode (tests/hosts inject an LLM): run the full pipeline once on the merged inputs.
679
+ // outlinePrompt 注入跨仓规则(Pass 1 统一大纲 + 命名空间 + 分仓小节 + 全局视角 + 按需读规约),
680
+ // 与 directive 模式同一套规则,避免库形态裸跑丢失跨仓约束。
681
+ try {
682
+ const result = await runDocument({
683
+ llm,
684
+ workRoot,
685
+ profile,
686
+ library,
687
+ inputs,
688
+ inputFeatures: inputFeatures(inputs, { includeSignoff: options.signoff }),
689
+ change: `synthesized:${dirs.map((d) => d.repo).join('+')}`,
690
+ maxRounds: options.signoff ? 3 : 2,
691
+ renderHtmlOutput: false,
692
+ persist: true,
693
+ outlinePrompt: synthRules,
694
+ projectRoot: options.workspaceRoot,
695
+ });
696
+ const payload = {
697
+ ok: result.ok,
698
+ stage: result.stage,
699
+ workRoot,
700
+ profile: profile.id,
701
+ scene,
702
+ document: result.documentMd ? join(workRoot, 'document.md') : undefined,
703
+ diagnostics: result.diagnostics,
704
+ repos: dirs.map((d) => `${d.repo}/${d.change}`),
705
+ };
706
+ if (options.json)
707
+ return { exitCode: result.ok ? 0 : 1, payload };
708
+ if (!result.ok) {
709
+ console.error(`Document synthesis failed at stage "${result.stage}":`);
710
+ for (const d of result.diagnostics)
711
+ console.error(` [${d.gate}][${d.severity}] ${d.message}`);
712
+ return { exitCode: 1, payload };
713
+ }
714
+ console.info(`跨仓合成文档已生成(${dirs.length} 仓,${profile.id}): ${join(workRoot, 'document.md')}`);
715
+ return { exitCode: 0, payload };
716
+ }
717
+ catch (error) {
718
+ const message = error instanceof Error ? error.message : String(error);
719
+ if (options.json)
720
+ return { exitCode: 1, payload: { ok: false, error: message } };
721
+ console.error(`Error: ${message}`);
722
+ return { exitCode: 1, payload: { ok: false, error: message } };
723
+ }
724
+ }
725
+ catch (error) {
726
+ // 顶层兜底(P2):discoverRepoChanges/loadProfile/fs.mkdir/fs.writeFile 等
727
+ // 任一步骤失败不再以未捕获 rejection 崩溃,--json 契约始终成立。
728
+ const message = error instanceof Error ? error.message : String(error);
729
+ if (options.json)
730
+ return { exitCode: 1, payload: { ok: false, error: message } };
731
+ console.error(`Error: ${message}`);
732
+ return { exitCode: 1, payload: { ok: false, error: message } };
733
+ }
734
+ }
735
+ // ============= registration =============
736
+ export function registerDocumentRunCommand(program) {
737
+ const doc = program
738
+ .command('techdoc')
739
+ .description('Generate scenario-based solution documents (Generic LongDoc Engine)');
740
+ doc
741
+ .command('detect')
742
+ .description('Auto-detect the scene profile (0to1 | frontend-0to1 | bugfix | feature | poc | migration) from natural language')
743
+ .option('--text <text>', 'Free-text problem description')
744
+ .option('--proposal <path>', 'Proposal input file')
745
+ .option('--design <path>', 'Design input file')
746
+ .option('--specs <path>', 'Specs input file')
747
+ .option('--code <path>', 'Code input file')
748
+ .option('--json', 'Output detection as JSON')
749
+ .action(async (opts) => {
750
+ const { exitCode, payload } = await detectDocumentCommand(opts);
751
+ if (exitCode !== 0)
752
+ process.exitCode = exitCode;
753
+ if (opts.json)
754
+ console.info(JSON.stringify(payload, null, 2));
755
+ });
756
+ doc
757
+ .command('plan <profile>')
758
+ .description('Emit the Agent-executable step list (IDE model does the work; no LLM API call)')
759
+ .option('--text <text>', 'Free-text problem description')
760
+ .option('--proposal <path>', 'Proposal input file')
761
+ .option('--design <path>', 'Design input file')
762
+ .option('--specs <path>', 'Specs input file')
763
+ .option('--code <path>', 'Code input file')
764
+ .option('--work-root <path>', 'Work directory (default: .specflow/document)')
765
+ .option('--json', 'Output step list as JSON')
766
+ .option('--markdown', 'Output step list as Markdown')
767
+ .action(async (profile, opts) => {
768
+ const { exitCode, payload } = await planDocumentCommand(profile, opts);
769
+ if (exitCode !== 0)
770
+ process.exitCode = exitCode;
771
+ if (opts.json)
772
+ console.info(JSON.stringify(payload, null, 2));
773
+ });
774
+ doc
775
+ .command('validate')
776
+ .description('Deterministically validate + render Agent-written artifacts (no LLM)')
777
+ .option('--work-root <path>', 'Work directory (default: .specflow/document)')
778
+ .option('--json', 'Output result as JSON')
779
+ .option('--no-render', 'Skip HTML rendering')
780
+ .action(async (opts) => {
781
+ const { exitCode, payload } = await validateDocumentCommand(opts);
782
+ if (exitCode !== 0)
783
+ process.exitCode = exitCode;
784
+ if (opts.json)
785
+ console.info(JSON.stringify(payload, null, 2));
786
+ });
787
+ doc
788
+ .command('run [profile]')
789
+ .description('One-click generate a scenario document. Omit <profile> to auto-detect scene; confirm when uncertain.')
790
+ .option('--text <text>', 'Free-text problem description')
791
+ .option('--proposal <path>', 'Proposal input file')
792
+ .option('--design <path>', 'Design input file')
793
+ .option('--specs <path>', 'Specs input file')
794
+ .option('--code <path>', 'Code input file')
795
+ .option('--work-root <path>', 'Work directory (default: .specflow/document)')
796
+ .option('--signoff', 'Include an approval sign-off section')
797
+ .option('--skip-review', 'Skip Pass 3 AI review')
798
+ .option('--max-rounds <n>', 'AI review max rounds', '3')
799
+ .option('--json', 'Output result as JSON')
800
+ .action(async (profile, opts) => {
801
+ const { exitCode, payload } = await runDocumentCommand(profile, opts);
802
+ if (exitCode !== 0)
803
+ process.exitCode = exitCode;
804
+ if (opts.json)
805
+ console.info(JSON.stringify(payload, null, 2));
806
+ });
807
+ doc
808
+ .command('synthesize')
809
+ .description('Synthesize multi-repo four artifacts into ONE cross-repo document (unified outline + namespaced contract entities + global overview/conclusion). Requires ≥2 repos. Omit --profile to auto-detect the scene (0to1/bugfix/feature/poc/migration) from --text or the artifacts; confirm when uncertain.')
810
+ .option('--workspace-root <path>', 'Multi-repo workspace root (required)')
811
+ .option('--changes <spec>', 'Multi-repo "repo1:change1,repo2:change2" (default: auto-scan workspaceRoot)')
812
+ .option('--text <text>', 'Natural-language description of the cross-repo work (used for scene detection when --profile is omitted)')
813
+ .option('--profile <id>', 'Synthesis profile (default: auto-detect scene from --text / artifacts; e.g. approve | 0to1 | bugfix | feature | poc | migration)')
814
+ .option('--work-root <path>', 'Output directory (default: <workspaceRoot>/.specflow/document-synthesized)')
815
+ .option('--signoff', 'Include the approval sign-off section')
816
+ .option('--json', 'Output result as JSON')
817
+ .action(async (opts) => {
818
+ const { exitCode, payload } = await synthesizeDocumentCommand(opts);
819
+ if (exitCode !== 0)
820
+ process.exitCode = exitCode;
821
+ if (opts.json)
822
+ console.info(JSON.stringify(payload, null, 2));
823
+ });
824
+ doc
825
+ .command('approve')
826
+ .description('Generate an approval document from the four artifacts (proposal/specs/design/tasks). Multi-repo with --workspace-root.')
827
+ .option('--proposal <path>', 'Proposal file')
828
+ .option('--specs <path>', 'Specs file')
829
+ .option('--design <path>', 'Design file')
830
+ .option('--tasks <path>', 'Tasks file')
831
+ .option('--change <name>', 'Change name (single-repo mode)')
832
+ .option('--workspace-root <path>', 'Multi-repo workspace root (scan per-repo four artifacts)')
833
+ .option('--changes <spec>', 'Multi-repo "repo1:change1,repo2:change2"')
834
+ .option('--work-root <path>', 'Work directory')
835
+ .option('--signoff', 'Include the approval sign-off section')
836
+ .option('--bundle', 'Merge each repo document.md into approval-unified.md (after all repos generated+validated)')
837
+ .option('--json', 'Output result as JSON')
838
+ .action(async (opts) => {
839
+ const { exitCode, payload } = await approveDocumentCommand(opts);
840
+ if (exitCode !== 0)
841
+ process.exitCode = exitCode;
842
+ if (opts.json)
843
+ console.info(JSON.stringify(payload, null, 2));
844
+ });
845
+ }