@gordon.gan/specflow 1.5.0-beta → 1.7.0-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 (39) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/approval-assemble.d.ts +48 -5
  3. package/dist/cli/commands/approval-assemble.js +347 -34
  4. package/dist/core/approval/assemble.js +15 -22
  5. package/dist/core/approval/bundle.d.ts +9 -0
  6. package/dist/core/approval/bundle.js +172 -0
  7. package/dist/core/approval/forbidden-patterns.d.ts +6 -0
  8. package/dist/core/approval/forbidden-patterns.js +37 -0
  9. package/dist/core/approval/index-schema.d.ts +203 -0
  10. package/dist/core/approval/index-schema.js +47 -0
  11. package/dist/core/approval/index.d.ts +10 -2
  12. package/dist/core/approval/index.js +7 -1
  13. package/dist/core/approval/lint.d.ts +10 -0
  14. package/dist/core/approval/lint.js +302 -0
  15. package/dist/core/approval/paths.d.ts +5 -0
  16. package/dist/core/approval/paths.js +15 -0
  17. package/dist/core/approval/pipeline.d.ts +28 -0
  18. package/dist/core/approval/pipeline.js +146 -0
  19. package/dist/core/approval/playbook-schema.d.ts +182 -0
  20. package/dist/core/approval/playbook-schema.js +51 -0
  21. package/dist/core/approval/render.d.ts +20 -0
  22. package/dist/core/approval/render.js +210 -0
  23. package/dist/core/approval/review-pack.d.ts +26 -0
  24. package/dist/core/approval/review-pack.js +205 -0
  25. package/dist/core/approval/types.d.ts +103 -0
  26. package/dist/integrations/shared/capability-evidence.js +2 -0
  27. package/dist/integrations/shared/parity-manifest.js +2 -0
  28. package/package.json +2 -1
  29. package/prompts/approval/acp-pipeline.md +104 -0
  30. package/prompts/approval/ai-review.md +145 -0
  31. package/prompts/approval/generate.md +94 -9
  32. package/prompts/approval/multi-repo-guidance.md +46 -10
  33. package/prompts/approval/runtime-guidance.md +64 -0
  34. package/prompts/approval/segmented-generation.md +14 -8
  35. package/skills/GUIDANCE_PACKS.md +1 -1
  36. package/skills/specflow-approval/SKILL.md +48 -25
  37. package/templates/approval-index.yaml +25 -4
  38. package/templates/approval-part.md +1 -1
  39. package/templates/approval-playbook.yaml +28 -0
package/README.md CHANGED
@@ -126,7 +126,7 @@ npm install -g @gordon.gan/specflow
126
126
  npm install -g github:Gordon-Gan-Jiang/specflow
127
127
 
128
128
  # 验证
129
- specflow --version # 以 npm / package.json 为准(当前 1.5.0-beta)
129
+ specflow --version # 以 npm / package.json 为准(当前 1.7.0-beta)
130
130
  specflow --help
131
131
  ```
132
132
 
@@ -282,7 +282,7 @@ specflow init
282
282
  | **explore** | 读代码、比方案、定边界;产出 `explore.md`,confirmed 后 handoff 到 propose |
283
283
  | **propose** | 一次产出 proposal、delta specs、design、tasks(第一轮深度思考,非占位骨架);叙述语言跟 `artifacts.language` |
284
284
  | **refine** | 内部多轮循环(≥2 轮,AI 判断收敛);可更新任意 artifact |
285
- | **approval** | refine 后可选:AI 对四件套做 7 维闭环性检查(含代码落地性、基线对照)+ 设计质量评估 + 可实施性评估,生成 `approval.md` 供人工审批;非轻量变更分片写入 `approval/parts/` `specflow approval assemble` 拼接;不改 phase,不阻塞 apply |
285
+ | **approval** | refine 后可选:AI 7 维闭环 + 设计质量 + 可实施性 `approval.md`;非轻量变更走 **ACP**:分片 `approval/parts/` Stage 12e AI Review → **`specflow approval finalize`**(lint / review-check / assemble / 多仓 bundle);不改 phase,不阻塞 apply |
286
286
  | **apply** | Phase A 重写 tasks.md;Phase B 按语言路由 ECC 审查 + TDD 逐任务执行;可选 `--yes` |
287
287
  | **review** | 代码审查,对照 specs 检查回归 |
288
288
  | **test** | 单元 + 集成 + E2E + 回归测试 |
@@ -1,21 +1,64 @@
1
1
  /**
2
- * CLI: specflow approval assemble | check
2
+ * CLI: specflow approval assemble | check | lint | bundle | review-pack | review-check | finalize
3
3
  *
4
- * Deterministic Reduce step for segmented approval generation.
5
- * No LLM merges approval/parts/*.md per approval/index.yaml.
4
+ * ACP (Approval Compiler Pipeline) — deterministic Reduce/Verify; Map is LLM in skill.
5
+ * Prefer `specflow approval finalize` for the full Verify → Reduce → bundle chain.
6
6
  */
7
7
  import type { Command } from 'commander';
8
- export interface ApprovalAssembleCommandOptions {
8
+ export interface ApprovalCommandOptions {
9
9
  readonly store?: string;
10
10
  readonly cwd?: string;
11
11
  readonly force?: boolean;
12
12
  readonly json?: boolean;
13
13
  readonly noLazyCheck?: boolean;
14
+ readonly strict?: boolean;
15
+ readonly playbook?: string;
16
+ readonly workspaceRoot?: string;
17
+ readonly dryRun?: boolean;
18
+ readonly requireReview?: boolean;
19
+ readonly skipReview?: boolean;
20
+ readonly bundle?: boolean;
21
+ readonly noBundle?: boolean;
22
+ readonly render?: boolean;
23
+ readonly renderInput?: string;
24
+ readonly renderOutput?: string;
14
25
  }
15
- export declare function runApprovalAssemble(changeName: string, options: ApprovalAssembleCommandOptions & {
26
+ export declare function runApprovalAssemble(changeName: string, options: ApprovalCommandOptions & {
16
27
  checkOnly?: boolean;
17
28
  }): Promise<{
18
29
  exitCode: number;
19
30
  payload: unknown;
20
31
  }>;
32
+ export declare function runApprovalLint(changeName: string, options: ApprovalCommandOptions): Promise<{
33
+ exitCode: number;
34
+ payload: unknown;
35
+ }>;
36
+ export declare function runApprovalBundle(changeName: string, options: ApprovalCommandOptions): Promise<{
37
+ exitCode: number;
38
+ payload: unknown;
39
+ }>;
40
+ export declare function runApprovalReviewPack(changeName: string, options: ApprovalCommandOptions): Promise<{
41
+ exitCode: number;
42
+ payload: unknown;
43
+ }>;
44
+ export declare function runApprovalReviewCheck(changeName: string, options: ApprovalCommandOptions): Promise<{
45
+ exitCode: number;
46
+ payload: unknown;
47
+ }>;
48
+ export declare function runApprovalReviewHashes(changeName: string, options: ApprovalCommandOptions): Promise<{
49
+ exitCode: number;
50
+ payload: unknown;
51
+ }>;
52
+ export declare function runApprovalRender(changeName: string, options: ApprovalCommandOptions & {
53
+ input?: string;
54
+ output?: string;
55
+ title?: string;
56
+ }): Promise<{
57
+ exitCode: number;
58
+ payload: unknown;
59
+ }>;
60
+ export declare function runApprovalFinalize(changeName: string, options: ApprovalCommandOptions): Promise<{
61
+ exitCode: number;
62
+ payload: unknown;
63
+ }>;
21
64
  export declare function registerApprovalAssembleCommand(program: Command): void;
@@ -1,13 +1,20 @@
1
1
  /**
2
- * CLI: specflow approval assemble | check
2
+ * CLI: specflow approval assemble | check | lint | bundle | review-pack | review-check | finalize
3
3
  *
4
- * Deterministic Reduce step for segmented approval generation.
5
- * No LLM merges approval/parts/*.md per approval/index.yaml.
4
+ * ACP (Approval Compiler Pipeline) — deterministic Reduce/Verify; Map is LLM in skill.
5
+ * Prefer `specflow approval finalize` for the full Verify → Reduce → bundle chain.
6
6
  */
7
7
  import { promises as fs } from 'node:fs';
8
8
  import { addStoreOption, resolveRootFromCommandOptions } from '../shared/store-option.js';
9
9
  import { validateChangeName } from '../../utils/change-utils.js';
10
- import { assembleApprovalDocument, getChangeDirectory, getApprovalOutputPath, } from '../../core/approval/index.js';
10
+ import { assembleApprovalDocument, bundleApprovalDocument, checkApprovalReview, generateReviewPacket, getApprovalOutputPath, getChangeDirectory, lintApprovalDocument, finalizeApprovalDocument, computeReviewPartHashes, readApprovalIndex, renderApprovalDocument, resolveApprovalRenderPaths, resolvePlaybookPath, } from '../../core/approval/index.js';
11
+ function printDiagnostics(diagnostics) {
12
+ for (const d of diagnostics) {
13
+ const prefix = d.severity === 'error' ? 'ERROR' : 'WARN';
14
+ const part = d.part ? ` [${d.part}]` : '';
15
+ console.error(`${prefix}${part}: ${d.message}`);
16
+ }
17
+ }
11
18
  export async function runApprovalAssemble(changeName, options) {
12
19
  validateChangeName(changeName);
13
20
  const projectRoot = await resolveRootFromCommandOptions({
@@ -21,6 +28,21 @@ export async function runApprovalAssemble(changeName, options) {
21
28
  catch {
22
29
  throw new Error(`Change "${changeName}" not found at ${changeDir}`);
23
30
  }
31
+ if (options.requireReview) {
32
+ const reviewCheck = await checkApprovalReview({
33
+ changeDir,
34
+ requirePass: true,
35
+ forceRequire: true,
36
+ });
37
+ if (!reviewCheck.ok) {
38
+ const payload = { ok: false, stage: 'review-check', diagnostics: reviewCheck.diagnostics };
39
+ if (options.json) {
40
+ return { exitCode: 1, payload };
41
+ }
42
+ printDiagnostics(reviewCheck.diagnostics);
43
+ return { exitCode: 1, payload };
44
+ }
45
+ }
24
46
  const result = await assembleApprovalDocument({
25
47
  changeDir,
26
48
  checkOnly: options.checkOnly ?? false,
@@ -38,11 +60,7 @@ export async function runApprovalAssemble(changeName, options) {
38
60
  return { exitCode: result.ok ? 0 : 1, payload };
39
61
  }
40
62
  if (result.diagnostics.length > 0) {
41
- for (const d of result.diagnostics) {
42
- const prefix = d.severity === 'error' ? 'ERROR' : 'WARN';
43
- const part = d.part ? ` [${d.part}]` : '';
44
- console.error(`${prefix}${part}: ${d.message}`);
45
- }
63
+ printDiagnostics(result.diagnostics);
46
64
  }
47
65
  if (result.ok) {
48
66
  if (options.checkOnly) {
@@ -55,41 +73,336 @@ export async function runApprovalAssemble(changeName, options) {
55
73
  }
56
74
  return { exitCode: 1, payload };
57
75
  }
76
+ export async function runApprovalLint(changeName, options) {
77
+ validateChangeName(changeName);
78
+ const projectRoot = await resolveRootFromCommandOptions({
79
+ store: options.store,
80
+ cwd: options.cwd,
81
+ });
82
+ const changeDir = getChangeDirectory(projectRoot, changeName);
83
+ const result = await lintApprovalDocument({
84
+ changeDir,
85
+ strict: options.strict ?? false,
86
+ });
87
+ const payload = { ok: result.ok, change: changeName, diagnostics: result.diagnostics };
88
+ if (options.json) {
89
+ return { exitCode: result.ok ? 0 : 1, payload };
90
+ }
91
+ printDiagnostics(result.diagnostics);
92
+ if (result.ok) {
93
+ console.info(`Lint passed for change "${changeName}".`);
94
+ return { exitCode: 0, payload };
95
+ }
96
+ return { exitCode: 1, payload };
97
+ }
98
+ export async function runApprovalBundle(changeName, options) {
99
+ validateChangeName(changeName);
100
+ const projectRoot = await resolveRootFromCommandOptions({
101
+ store: options.store,
102
+ cwd: options.cwd,
103
+ });
104
+ const workspaceRoot = options.workspaceRoot ?? projectRoot;
105
+ const changeDir = getChangeDirectory(projectRoot, changeName);
106
+ const playbookPath = await resolvePlaybookPath(changeDir, options.playbook);
107
+ const result = await bundleApprovalDocument({
108
+ workspaceRoot,
109
+ changeDir,
110
+ playbookPath,
111
+ write: !options.dryRun,
112
+ });
113
+ const payload = {
114
+ ok: result.ok,
115
+ change: changeName,
116
+ output: result.outputPath,
117
+ playbook: playbookPath,
118
+ dry_run: options.dryRun ?? false,
119
+ diagnostics: result.diagnostics,
120
+ };
121
+ if (options.json) {
122
+ return { exitCode: result.ok ? 0 : 1, payload };
123
+ }
124
+ printDiagnostics(result.diagnostics);
125
+ if (result.ok) {
126
+ if (options.dryRun) {
127
+ console.info(`Bundle dry-run OK for "${changeName}" → ${result.outputPath}`);
128
+ }
129
+ else {
130
+ console.info(`Wrote bundle ${result.outputPath}`);
131
+ }
132
+ return { exitCode: 0, payload };
133
+ }
134
+ return { exitCode: 1, payload };
135
+ }
136
+ export async function runApprovalReviewPack(changeName, options) {
137
+ validateChangeName(changeName);
138
+ const projectRoot = await resolveRootFromCommandOptions({
139
+ store: options.store,
140
+ cwd: options.cwd,
141
+ });
142
+ const changeDir = getChangeDirectory(projectRoot, changeName);
143
+ const result = await generateReviewPacket({ changeDir });
144
+ const payload = {
145
+ ok: result.ok,
146
+ change: changeName,
147
+ packet_path: result.packetPath,
148
+ diagnostics: result.diagnostics,
149
+ };
150
+ if (options.json) {
151
+ return { exitCode: result.ok ? 0 : 1, payload };
152
+ }
153
+ printDiagnostics(result.diagnostics);
154
+ if (result.ok) {
155
+ console.info(`Wrote review packet ${result.packetPath}`);
156
+ console.info('Run AI review (Stage 12e), then: specflow approval review-check');
157
+ return { exitCode: 0, payload };
158
+ }
159
+ return { exitCode: 1, payload };
160
+ }
161
+ export async function runApprovalReviewCheck(changeName, options) {
162
+ validateChangeName(changeName);
163
+ const projectRoot = await resolveRootFromCommandOptions({
164
+ store: options.store,
165
+ cwd: options.cwd,
166
+ });
167
+ const changeDir = getChangeDirectory(projectRoot, changeName);
168
+ const result = await checkApprovalReview({
169
+ changeDir,
170
+ requirePass: options.requireReview !== false,
171
+ });
172
+ const payload = {
173
+ ok: result.ok,
174
+ change: changeName,
175
+ verdict: result.review?.verdict,
176
+ diagnostics: result.diagnostics,
177
+ };
178
+ if (options.json) {
179
+ return { exitCode: result.ok ? 0 : 1, payload };
180
+ }
181
+ printDiagnostics(result.diagnostics);
182
+ if (result.ok) {
183
+ console.info(`Review check passed for "${changeName}" (verdict=${result.review?.verdict ?? 'n/a'}).`);
184
+ return { exitCode: 0, payload };
185
+ }
186
+ return { exitCode: 1, payload };
187
+ }
188
+ export async function runApprovalReviewHashes(changeName, options) {
189
+ validateChangeName(changeName);
190
+ const projectRoot = await resolveRootFromCommandOptions({
191
+ store: options.store,
192
+ cwd: options.cwd,
193
+ });
194
+ const changeDir = getChangeDirectory(projectRoot, changeName);
195
+ let index;
196
+ try {
197
+ index = await readApprovalIndex(changeDir);
198
+ }
199
+ catch (error) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ throw new Error(`Cannot read index: ${message}`);
202
+ }
203
+ const part_hashes = await computeReviewPartHashes(changeDir, index.parts_order);
204
+ const payload = { change: changeName, part_hashes };
205
+ if (options.json) {
206
+ return { exitCode: 0, payload };
207
+ }
208
+ console.info(JSON.stringify(part_hashes, null, 2));
209
+ return { exitCode: 0, payload };
210
+ }
211
+ export async function runApprovalRender(changeName, options) {
212
+ validateChangeName(changeName);
213
+ const projectRoot = await resolveRootFromCommandOptions({
214
+ store: options.store,
215
+ cwd: options.cwd,
216
+ });
217
+ const changeDir = getChangeDirectory(projectRoot, changeName);
218
+ const paths = resolveApprovalRenderPaths(changeDir, options.input, options.output);
219
+ const result = await renderApprovalDocument({
220
+ changeDir,
221
+ inputPath: paths.inputPath,
222
+ outputPath: paths.outputPath,
223
+ title: options.title,
224
+ write: !options.dryRun,
225
+ });
226
+ const payload = {
227
+ ok: result.ok,
228
+ change: changeName,
229
+ input: paths.inputPath,
230
+ output: result.outputPath,
231
+ sha256: result.sha256,
232
+ dry_run: options.dryRun ?? false,
233
+ diagnostics: result.diagnostics,
234
+ };
235
+ if (options.json) {
236
+ return { exitCode: result.ok ? 0 : 1, payload };
237
+ }
238
+ if (result.diagnostics.length > 0) {
239
+ printDiagnostics(result.diagnostics);
240
+ }
241
+ if (result.ok) {
242
+ if (options.dryRun) {
243
+ console.info(`Render dry-run OK for "${changeName}" → ${result.outputPath}`);
244
+ }
245
+ else {
246
+ console.info(`Wrote ${result.outputPath}`);
247
+ }
248
+ return { exitCode: 0, payload };
249
+ }
250
+ return { exitCode: 1, payload };
251
+ }
252
+ export async function runApprovalFinalize(changeName, options) {
253
+ validateChangeName(changeName);
254
+ const projectRoot = await resolveRootFromCommandOptions({
255
+ store: options.store,
256
+ cwd: options.cwd,
257
+ });
258
+ const changeDir = getChangeDirectory(projectRoot, changeName);
259
+ const bundleMode = options.noBundle ? false : options.bundle ? true : 'auto';
260
+ const result = await finalizeApprovalDocument({
261
+ changeDir,
262
+ workspaceRoot: options.workspaceRoot ?? projectRoot,
263
+ strict: options.strict ?? true,
264
+ skipReview: options.skipReview ?? false,
265
+ force: options.force ?? true,
266
+ bundle: bundleMode,
267
+ playbook: options.playbook,
268
+ dryRun: options.dryRun ?? false,
269
+ validateLazy: !options.noLazyCheck,
270
+ });
271
+ const payload = {
272
+ ok: result.ok,
273
+ paused: result.paused ?? false,
274
+ stage: result.stage,
275
+ change: changeName,
276
+ output: result.outputPath,
277
+ bundle: result.bundlePath,
278
+ review_packet: result.reviewPacketPath,
279
+ diagnostics: result.diagnostics,
280
+ };
281
+ if (options.json) {
282
+ const exitCode = result.ok ? 0 : result.paused ? 2 : 1;
283
+ return { exitCode, payload };
284
+ }
285
+ printDiagnostics(result.diagnostics);
286
+ if (result.paused) {
287
+ console.info(`Paused at AI review for "${changeName}". Complete Stage 12e in-session, then re-run finalize.`);
288
+ if (result.reviewPacketPath) {
289
+ console.info(`Review packet: ${result.reviewPacketPath}`);
290
+ }
291
+ return { exitCode: 2, payload };
292
+ }
293
+ if (result.ok) {
294
+ console.info(`Finalized approval for "${changeName}" (stage=${result.stage}).`);
295
+ if (result.outputPath) {
296
+ console.info(`Wrote ${result.outputPath}`);
297
+ }
298
+ if (result.bundlePath) {
299
+ console.info(`Wrote bundle ${result.bundlePath}`);
300
+ }
301
+ if (options.render && !options.dryRun) {
302
+ const paths = resolveApprovalRenderPaths(changeDir, options.renderInput, options.renderOutput);
303
+ const renderResult = await renderApprovalDocument({
304
+ changeDir,
305
+ inputPath: paths.inputPath,
306
+ outputPath: paths.outputPath,
307
+ write: true,
308
+ });
309
+ if (renderResult.ok && renderResult.outputPath) {
310
+ console.info(`Wrote ${renderResult.outputPath}`);
311
+ }
312
+ else {
313
+ printDiagnostics(renderResult.diagnostics);
314
+ return { exitCode: 1, payload: { ...payload, render: renderResult } };
315
+ }
316
+ }
317
+ return { exitCode: 0, payload };
318
+ }
319
+ console.error(`Finalize failed at stage "${result.stage}" for "${changeName}".`);
320
+ return { exitCode: 1, payload };
321
+ }
322
+ function bindJsonAction(action) {
323
+ return async (change, opts) => {
324
+ const { exitCode, payload } = await action(change, opts);
325
+ if (opts.json) {
326
+ console.info(JSON.stringify(payload, null, 2));
327
+ }
328
+ if (exitCode !== 0) {
329
+ process.exitCode = exitCode;
330
+ }
331
+ };
332
+ }
58
333
  export function registerApprovalAssembleCommand(program) {
59
- const approval = program.command('approval').description('Approval document utilities');
334
+ const approval = program.command('approval').description('Approval document utilities (ACP)');
335
+ addStoreOption(approval
336
+ .command('finalize <change>')
337
+ .description('Run full ACP finalize: lint → review-check → check → assemble → bundle (preferred entry)')
338
+ .option('--skip-review', 'Skip AI review gate even when acp.review is enabled')
339
+ .option('--bundle', 'Force multi-repo bundle after assemble')
340
+ .option('--no-bundle', 'Skip bundle even when multi_repo.bundle.enabled')
341
+ .option('--playbook <path>', 'Path to playbook YAML (bundle step)')
342
+ .option('--workspace-root <path>', 'Workspace root for multi-repo bundle')
343
+ .option('--force', 'Overwrite existing approval.md (default: true)', true)
344
+ .option('--dry-run', 'Validate through check without writing approval.md or bundle')
345
+ .option('--strict', 'Strict lint (default: true)', true)
346
+ .option('--json', 'Output result as JSON')
347
+ .option('--no-lazy-check', 'Skip stub/lazy content validation on parts')
348
+ .option('--render', 'Also render approval.md to approval.html after finalize')
349
+ .option('--render-input <path>', 'Markdown input for --render (default: approval.md)')
350
+ .option('--render-output <path>', 'HTML output for --render (default: approval.html)')
351
+ .action(bindJsonAction(runApprovalFinalize)));
352
+ addStoreOption(approval
353
+ .command('render <change>')
354
+ .description('Render approval.md (or bundle markdown) to standalone HTML')
355
+ .option('--input <path>', 'Input markdown (default: approval.md)')
356
+ .option('--output <path>', 'Output HTML (default: same name with .html)')
357
+ .option('--title <text>', 'HTML document title override')
358
+ .option('--dry-run', 'Validate render without writing HTML file')
359
+ .option('--json', 'Output result as JSON')
360
+ .action(bindJsonAction((change, opts) => runApprovalRender(change, opts))));
60
361
  addStoreOption(approval
61
362
  .command('assemble <change>')
62
363
  .description('Merge approval/parts into approval.md (deterministic, no LLM)')
63
364
  .option('--force', 'Overwrite existing approval.md')
365
+ .option('--require-review', 'Require approval/review-result.json pass before assemble')
64
366
  .option('--json', 'Output result as JSON')
65
367
  .option('--no-lazy-check', 'Skip stub/lazy content validation on parts')
66
- .action(async (change, opts) => {
67
- const { exitCode, payload } = await runApprovalAssemble(change, {
68
- ...opts,
69
- checkOnly: false,
70
- });
71
- if (opts.json) {
72
- console.info(JSON.stringify(payload, null, 2));
73
- }
74
- if (exitCode !== 0) {
75
- process.exitCode = exitCode;
76
- }
77
- }));
368
+ .action(bindJsonAction((change, opts) => runApprovalAssemble(change, { ...opts, checkOnly: false }))));
78
369
  addStoreOption(approval
79
370
  .command('check <change>')
80
371
  .description('Validate approval parts and index without writing approval.md')
81
372
  .option('--json', 'Output result as JSON')
82
373
  .option('--no-lazy-check', 'Skip stub/lazy content validation on parts')
83
- .action(async (change, opts) => {
84
- const { exitCode, payload } = await runApprovalAssemble(change, {
85
- ...opts,
86
- checkOnly: true,
87
- });
88
- if (opts.json) {
89
- console.info(JSON.stringify(payload, null, 2));
90
- }
91
- if (exitCode !== 0) {
92
- process.exitCode = exitCode;
93
- }
94
- }));
374
+ .action(bindJsonAction((change, opts) => runApprovalAssemble(change, { ...opts, checkOnly: true }))));
375
+ addStoreOption(approval
376
+ .command('lint <change>')
377
+ .description('Cross-part consistency lint (IR freeze, pairs_with, page apis)')
378
+ .option('--strict', 'Treat inconsistency hints as errors')
379
+ .option('--json', 'Output result as JSON')
380
+ .action(bindJsonAction(runApprovalLint)));
381
+ addStoreOption(approval
382
+ .command('bundle <change>')
383
+ .description('Deterministic multi-repo bundle view from approval-playbook.yaml')
384
+ .option('--playbook <path>', 'Path to playbook YAML')
385
+ .option('--workspace-root <path>', 'Workspace root containing multiple repo planning roots')
386
+ .option('--dry-run', 'Validate bundle without writing output file')
387
+ .option('--json', 'Output result as JSON')
388
+ .action(bindJsonAction(runApprovalBundle)));
389
+ addStoreOption(approval
390
+ .command('review-pack <change>')
391
+ .description('Generate AI review packet (review-packet.json) for Stage 12e')
392
+ .option('--json', 'Output result as JSON')
393
+ .action(bindJsonAction(runApprovalReviewPack)));
394
+ addStoreOption(approval
395
+ .command('review-hashes <change>')
396
+ .description('Print part_hashes[] JSON for review-result.json after Stage 12e fixes')
397
+ .option('--json', 'Output result as JSON')
398
+ .action(bindJsonAction(runApprovalReviewHashes)));
399
+ addStoreOption(approval
400
+ .command('review-check <change>')
401
+ .description('Validate review-result.json + strict lint before assemble')
402
+ .option('--no-require-review', 'Skip requiring review-result when acp.review not enabled')
403
+ .option('--json', 'Output result as JSON')
404
+ .action(bindJsonAction((change, opts) => runApprovalReviewCheck(change, {
405
+ ...opts,
406
+ requireReview: opts.noRequireReview ? false : true,
407
+ }))));
95
408
  }
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import yaml from 'js-yaml';
4
4
  import { parseApprovalIndex } from './index-schema.js';
5
+ import { findForbiddenMatch } from './forbidden-patterns.js';
5
6
  import { getApprovalIndexPath, getApprovalManifestPath, getApprovalOutputPath, getApprovalPartPath, getApprovalPartsDir, } from './paths.js';
6
7
  const OPTIONAL_PREFIXES = {
7
8
  s5: ['05-', '05.'],
@@ -21,22 +22,6 @@ const CHAPTER_HEADERS = {
21
22
  'appendix-a': '## 附录 A: 产物溯源',
22
23
  };
23
24
  const SECTION4_HEADER = '## 4. 方案详细设计 (Detailed Design)';
24
- const LAZY_PATTERNS = [
25
- /\bTODO\b/i,
26
- /待补充/,
27
- /待 refine 澄清(?!.*\[)/,
28
- /此处省略/,
29
- /详见\s*(?:上文|下文|附件)(?!.*§)/,
30
- /略\s*[。.]?$/,
31
- /TBD\s*[。.]?$/,
32
- /(?:^|\n)\s*\.{3}\s*(?:\n|$)/,
33
- /(?:^|\n)\s*(?:同上|同前)\s*[。.]?\s*(?:\n|$)/,
34
- /实现时(?:命名|对齐)/,
35
- /(?:RPC|rpc).*暂定/,
36
- /暂定\s*[`']?\w+[`']?/,
37
- /\b如\s+[A-Z]\w*(?:Result|Request|Response)/,
38
- /内部(?:经|调用)\s*I\d+[^.\n]{0,40}(?:一行|代替|省略)/,
39
- ];
40
25
  const MIN_PART_BYTES = {
41
26
  '04.4': 200,
42
27
  '04.5': 200,
@@ -85,10 +70,9 @@ function detectLazyContent(partId, content) {
85
70
  return `part content below minimum for ${prefix} (${trimmed.length} bytes)`;
86
71
  }
87
72
  }
88
- for (const pattern of LAZY_PATTERNS) {
89
- if (pattern.test(trimmed)) {
90
- return `lazy/stub pattern detected: ${pattern.source}`;
91
- }
73
+ const forbidden = findForbiddenMatch(trimmed);
74
+ if (forbidden) {
75
+ return `lazy/stub pattern detected: ${forbidden.source}`;
92
76
  }
93
77
  if (/^#+\s*4\.\s/m.test(trimmed)) {
94
78
  return 'part must not contain top-level "## 4." heading (assemble injects section 4)';
@@ -101,6 +85,14 @@ function detectLazyContent(partId, content) {
101
85
  }
102
86
  return null;
103
87
  }
88
+ function partHasAppendixA(partContents) {
89
+ for (const content of partContents.values()) {
90
+ if (/^#{2,3}\s+附录\s*A/m.test(content)) {
91
+ return true;
92
+ }
93
+ }
94
+ return false;
95
+ }
104
96
  function buildDocumentHeader(index) {
105
97
  const meta = index.meta ?? {};
106
98
  const lang = meta.language ?? 'en';
@@ -112,11 +104,12 @@ function buildDocumentHeader(index) {
112
104
  if (mr?.enabled && mr.repos.length >= 2) {
113
105
  const mode = mr.document_mode ?? 'pending';
114
106
  const primary = mr.primary_repo ?? '—';
107
+ const bundleNote = mr.bundle?.enabled ? ' | 合订: approval bundle (只读)' : '';
115
108
  const repoLines = mr.repos
116
109
  .map((r) => `- ${r.label} \`${r.id}/${r.change}\``)
117
110
  .join('\n');
118
111
  multiRepoBlock = `
119
- > **多仓范围** (${mode}${mode === 'unified' ? `, 主仓: ${primary}` : ''}):
112
+ > **多仓范围** (${mode}${mode === 'unified' ? `, 主仓: ${primary}` : mr.bundle?.enabled ? `, bundle 主仓: ${primary}` : ''}${bundleNote}):
120
113
  ${repoLines}
121
114
  `;
122
115
  }
@@ -279,7 +272,7 @@ export async function assembleApprovalDocument(options) {
279
272
  }
280
273
  body.push(content.trim(), '', '---', '');
281
274
  }
282
- if (!index.parts_order.includes('appendix-a')) {
275
+ if (!index.parts_order.includes('appendix-a') && !partHasAppendixA(partContents)) {
283
276
  body.push(buildAppendix().trim());
284
277
  }
285
278
  for (const table of index.tables) {
@@ -0,0 +1,9 @@
1
+ import type { ApprovalBundleResult } from './types.js';
2
+ export interface BundleApprovalOptions {
3
+ readonly workspaceRoot: string;
4
+ readonly changeDir: string;
5
+ readonly playbookPath: string;
6
+ readonly write?: boolean;
7
+ }
8
+ export declare function bundleApprovalDocument(options: BundleApprovalOptions): Promise<ApprovalBundleResult>;
9
+ export declare function resolvePlaybookPath(changeDir: string, explicitPath?: string): Promise<string>;