openfeel 1.1.1 → 1.1.2

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 (196) hide show
  1. package/README.en.md +41 -11
  2. package/README.md +8 -3
  3. package/README.zh-CN.md +42 -12
  4. package/bin/openfeel.js +5 -0
  5. package/dist/cli/index.d.ts +15 -3
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +59 -15
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/output-encoding.d.ts +41 -0
  10. package/dist/cli/output-encoding.d.ts.map +1 -0
  11. package/dist/cli/output-encoding.js +165 -0
  12. package/dist/cli/output-encoding.js.map +1 -0
  13. package/dist/cli/repl.d.ts +2 -2
  14. package/dist/cli/repl.d.ts.map +1 -1
  15. package/dist/cli/repl.js +46 -25
  16. package/dist/cli/repl.js.map +1 -1
  17. package/dist/commands/config.d.ts.map +1 -1
  18. package/dist/commands/config.js +56 -9
  19. package/dist/commands/config.js.map +1 -1
  20. package/dist/commands/flow.d.ts.map +1 -1
  21. package/dist/commands/flow.js +827 -138
  22. package/dist/commands/flow.js.map +1 -1
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +56 -42
  25. package/dist/commands/init.js.map +1 -1
  26. package/dist/commands/knowledge.d.ts.map +1 -1
  27. package/dist/commands/knowledge.js +69 -0
  28. package/dist/commands/knowledge.js.map +1 -1
  29. package/dist/commands/lint.d.ts.map +1 -1
  30. package/dist/commands/lint.js +17 -5
  31. package/dist/commands/lint.js.map +1 -1
  32. package/dist/commands/migrate.d.ts.map +1 -1
  33. package/dist/commands/migrate.js +6 -1
  34. package/dist/commands/migrate.js.map +1 -1
  35. package/dist/commands/model.d.ts.map +1 -1
  36. package/dist/commands/model.js +18 -1
  37. package/dist/commands/model.js.map +1 -1
  38. package/dist/commands/plan.d.ts.map +1 -1
  39. package/dist/commands/plan.js +185 -7
  40. package/dist/commands/plan.js.map +1 -1
  41. package/dist/commands/project.js +2 -2
  42. package/dist/commands/project.js.map +1 -1
  43. package/dist/commands/roadmap.d.ts.map +1 -1
  44. package/dist/commands/roadmap.js +20 -5
  45. package/dist/commands/roadmap.js.map +1 -1
  46. package/dist/commands/setup.d.ts +1 -1
  47. package/dist/commands/setup.d.ts.map +1 -1
  48. package/dist/commands/setup.js +8 -2
  49. package/dist/commands/setup.js.map +1 -1
  50. package/dist/commands/shared/errors.d.ts +22 -0
  51. package/dist/commands/shared/errors.d.ts.map +1 -0
  52. package/dist/commands/shared/errors.js +45 -0
  53. package/dist/commands/shared/errors.js.map +1 -0
  54. package/dist/commands/stage.d.ts.map +1 -1
  55. package/dist/commands/stage.js +133 -23
  56. package/dist/commands/stage.js.map +1 -1
  57. package/dist/commands/view.d.ts +1 -1
  58. package/dist/commands/view.d.ts.map +1 -1
  59. package/dist/commands/view.js +4 -26
  60. package/dist/commands/view.js.map +1 -1
  61. package/dist/core/archive/merge.d.ts.map +1 -1
  62. package/dist/core/archive/merge.js +9 -5
  63. package/dist/core/archive/merge.js.map +1 -1
  64. package/dist/core/artifact-graph/graph.d.ts +7 -2
  65. package/dist/core/artifact-graph/graph.d.ts.map +1 -1
  66. package/dist/core/artifact-graph/graph.js +25 -18
  67. package/dist/core/artifact-graph/graph.js.map +1 -1
  68. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  69. package/dist/core/artifact-graph/instruction-loader.js +4 -33
  70. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  71. package/dist/core/backup.d.ts +28 -0
  72. package/dist/core/backup.d.ts.map +1 -0
  73. package/dist/core/backup.js +118 -0
  74. package/dist/core/backup.js.map +1 -0
  75. package/dist/core/config.d.ts +30 -19
  76. package/dist/core/config.d.ts.map +1 -1
  77. package/dist/core/config.js +86 -26
  78. package/dist/core/config.js.map +1 -1
  79. package/dist/core/flow-manager.d.ts +256 -5
  80. package/dist/core/flow-manager.d.ts.map +1 -1
  81. package/dist/core/flow-manager.js +908 -68
  82. package/dist/core/flow-manager.js.map +1 -1
  83. package/dist/core/fs/atomic-write.d.ts +3 -0
  84. package/dist/core/fs/atomic-write.d.ts.map +1 -1
  85. package/dist/core/fs/atomic-write.js +3 -0
  86. package/dist/core/fs/atomic-write.js.map +1 -1
  87. package/dist/core/fs/safe-read.d.ts +11 -0
  88. package/dist/core/fs/safe-read.d.ts.map +1 -0
  89. package/dist/core/fs/safe-read.js +31 -0
  90. package/dist/core/fs/safe-read.js.map +1 -0
  91. package/dist/core/global-paths.d.ts +16 -7
  92. package/dist/core/global-paths.d.ts.map +1 -1
  93. package/dist/core/global-paths.js +23 -8
  94. package/dist/core/global-paths.js.map +1 -1
  95. package/dist/core/i18n-data/en.d.ts +2 -0
  96. package/dist/core/i18n-data/en.d.ts.map +1 -1
  97. package/dist/core/i18n-data/en.js +259 -23
  98. package/dist/core/i18n-data/en.js.map +1 -1
  99. package/dist/core/i18n-data/zh-CN.d.ts +2 -0
  100. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  101. package/dist/core/i18n-data/zh-CN.js +265 -23
  102. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  103. package/dist/core/i18n.d.ts +8 -0
  104. package/dist/core/i18n.d.ts.map +1 -1
  105. package/dist/core/i18n.js +14 -4
  106. package/dist/core/i18n.js.map +1 -1
  107. package/dist/core/init.d.ts +4 -2
  108. package/dist/core/init.d.ts.map +1 -1
  109. package/dist/core/init.js +38 -13
  110. package/dist/core/init.js.map +1 -1
  111. package/dist/core/managed-region.d.ts +1 -1
  112. package/dist/core/managed-region.js +1 -1
  113. package/dist/core/metrics.d.ts +16 -4
  114. package/dist/core/metrics.d.ts.map +1 -1
  115. package/dist/core/metrics.js +31 -11
  116. package/dist/core/metrics.js.map +1 -1
  117. package/dist/core/migrate.d.ts.map +1 -1
  118. package/dist/core/migrate.js +53 -19
  119. package/dist/core/migrate.js.map +1 -1
  120. package/dist/core/model-config.d.ts.map +1 -1
  121. package/dist/core/model-config.js +4 -2
  122. package/dist/core/model-config.js.map +1 -1
  123. package/dist/core/opencode-config.d.ts +4 -4
  124. package/dist/core/opencode-config.d.ts.map +1 -1
  125. package/dist/core/opencode-config.js +6 -5
  126. package/dist/core/opencode-config.js.map +1 -1
  127. package/dist/core/plan/path.d.ts +40 -0
  128. package/dist/core/plan/path.d.ts.map +1 -1
  129. package/dist/core/plan/path.js +108 -1
  130. package/dist/core/plan/path.js.map +1 -1
  131. package/dist/core/plan/roadmap.js +2 -2
  132. package/dist/core/plan/roadmap.js.map +1 -1
  133. package/dist/core/plan/scheme.d.ts +74 -2
  134. package/dist/core/plan/scheme.d.ts.map +1 -1
  135. package/dist/core/plan/scheme.js +311 -26
  136. package/dist/core/plan/scheme.js.map +1 -1
  137. package/dist/core/plan/stage.d.ts +34 -3
  138. package/dist/core/plan/stage.d.ts.map +1 -1
  139. package/dist/core/plan/stage.js +90 -6
  140. package/dist/core/plan/stage.js.map +1 -1
  141. package/dist/core/public-logger.d.ts +18 -5
  142. package/dist/core/public-logger.d.ts.map +1 -1
  143. package/dist/core/public-logger.js +62 -23
  144. package/dist/core/public-logger.js.map +1 -1
  145. package/dist/core/runtime-log.d.ts +25 -0
  146. package/dist/core/runtime-log.d.ts.map +1 -0
  147. package/dist/core/runtime-log.js +94 -0
  148. package/dist/core/runtime-log.js.map +1 -0
  149. package/dist/core/setup.d.ts.map +1 -1
  150. package/dist/core/setup.js +47 -19
  151. package/dist/core/setup.js.map +1 -1
  152. package/dist/core/template-loader.d.ts.map +1 -1
  153. package/dist/core/template-loader.js +672 -288
  154. package/dist/core/template-loader.js.map +1 -1
  155. package/dist/core/templates.d.ts +2 -2
  156. package/dist/core/templates.d.ts.map +1 -1
  157. package/dist/core/templates.js +13 -9
  158. package/dist/core/templates.js.map +1 -1
  159. package/dist/core/update-infos.d.ts +14 -3
  160. package/dist/core/update-infos.d.ts.map +1 -1
  161. package/dist/core/update-infos.js +67 -15
  162. package/dist/core/update-infos.js.map +1 -1
  163. package/dist/core/update-state.d.ts.map +1 -1
  164. package/dist/core/update-state.js +7 -4
  165. package/dist/core/update-state.js.map +1 -1
  166. package/dist/core/update.d.ts +2 -1
  167. package/dist/core/update.d.ts.map +1 -1
  168. package/dist/core/update.js +253 -61
  169. package/dist/core/update.js.map +1 -1
  170. package/dist/core/view/entry.d.ts +53 -1
  171. package/dist/core/view/entry.d.ts.map +1 -1
  172. package/dist/core/view/entry.js +71 -2
  173. package/dist/core/view/entry.js.map +1 -1
  174. package/dist/core/workspace/identity.d.ts.map +1 -1
  175. package/dist/core/workspace/identity.js +15 -5
  176. package/dist/core/workspace/identity.js.map +1 -1
  177. package/dist/core/workspace/knowledge.d.ts.map +1 -1
  178. package/dist/core/workspace/knowledge.js +125 -19
  179. package/dist/core/workspace/knowledge.js.map +1 -1
  180. package/dist/core/workspace/structure.d.ts.map +1 -1
  181. package/dist/core/workspace/structure.js +6 -2
  182. package/dist/core/workspace/structure.js.map +1 -1
  183. package/dist/index.d.ts +0 -1
  184. package/dist/index.d.ts.map +1 -1
  185. package/dist/index.js +1 -2
  186. package/dist/index.js.map +1 -1
  187. package/dist/utils/kb-dedup.d.ts +5 -1
  188. package/dist/utils/kb-dedup.d.ts.map +1 -1
  189. package/dist/utils/kb-dedup.js +11 -5
  190. package/dist/utils/kb-dedup.js.map +1 -1
  191. package/package.json +6 -6
  192. package/dist/utils/path.d.ts +0 -22
  193. package/dist/utils/path.d.ts.map +0 -1
  194. package/dist/utils/path.js +0 -37
  195. package/dist/utils/path.js.map +0 -1
  196. package/scripts/patch-inquirer.js +0 -70
@@ -1,9 +1,12 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { existsSync, copyFileSync, readFileSync } from 'node:fs';
2
+ import { existsSync, copyFileSync, readFileSync, rmSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { FlowManager, isFlowConcurrentError, normalizeAgentName } from '../core/flow-manager.js';
4
+ import { FlowManager, isFlowConcurrentError, normalizeAgentName, detectFillState, PHASE_PATH_MAX_DEPTH } from '../core/flow-manager.js';
5
5
  import { PipelinePhaseSchema, PIPELINE_PHASES } from '../core/pipeline-schema.js';
6
+ import { validateStageId, suggestStageId, normalizeStageId } from '../core/plan/path.js';
6
7
  import { MetricsStore } from '../core/metrics.js';
8
+ import { addReviewEntry } from '../core/view/entry.js';
9
+ import { handleAddStageError, handleConcurrentConflict } from './shared/errors.js';
7
10
  import { t, getCliLang } from '../core/i18n.js';
8
11
  export function registerFlowCommand(program) {
9
12
  const flow = program
@@ -15,9 +18,15 @@ export function registerFlowCommand(program) {
15
18
  .description('显示流水线状态摘要')
16
19
  .option('--verbose', '增强输出:配置级联、最近状态变更、下游 Agent 就绪状态')
17
20
  .option('-n, --lines <n>', '最近状态变更条数(默认 5)', '5')
21
+ .option('--json', 'Output as JSON (machine-readable, single JSON document)')
18
22
  .action((options) => {
19
23
  const lang = getCliLang(process.cwd());
20
24
  const mgr = createManager();
25
+ // B1-1:--json 分支须为首行输出且纯 JSON 单文档(不打印标题/颜色/提示行)
26
+ if (options.json) {
27
+ console.log(JSON.stringify(buildStatusJson(mgr), null, 2));
28
+ return;
29
+ }
21
30
  if (!options.verbose) {
22
31
  console.log(mgr.summary(lang));
23
32
  // 各阶段 phase 展示
@@ -59,6 +68,7 @@ export function registerFlowCommand(program) {
59
68
  // ── 配置级联状态 ──
60
69
  console.log(t('flow.status.cascadeTitle', lang));
61
70
  const allKeys = new Set([
71
+ ...Object.keys(v.cascade.profileDefaults),
62
72
  ...Object.keys(v.cascade.configDefaults),
63
73
  ...Object.keys(v.cascade.statusOverrides),
64
74
  ]);
@@ -69,11 +79,12 @@ export function registerFlowCommand(program) {
69
79
  console.log(t('flow.status.cascadeHeader', lang));
70
80
  console.log('─────────────────────────────────────────────────');
71
81
  for (const key of [...allKeys].sort()) {
82
+ const prof = v.cascade.profileDefaults[key] ?? '-';
72
83
  const def = v.cascade.configDefaults[key] ?? '-';
73
84
  const over = v.cascade.statusOverrides[key] ?? '-';
74
85
  const eff = v.cascade.effective[key] ?? '-';
75
86
  const overFlag = v.cascade.statusOverrides[key] ? '*' : ' ';
76
- console.log(`${key.padEnd(18)} ${def.padEnd(12)} ${overFlag}${over.padEnd(11)} ${eff}`);
87
+ console.log(`${key.padEnd(18)} ${prof.padEnd(12)} ${def.padEnd(12)} ${overFlag}${over.padEnd(11)} ${eff}`);
77
88
  }
78
89
  console.log(t('flow.status.cascadeNote', lang));
79
90
  }
@@ -127,13 +138,24 @@ export function registerFlowCommand(program) {
127
138
  flow
128
139
  .command('overview')
129
140
  .description('全状态可视化视图(openfeel flow overview 的后端实现)')
130
- .action(() => {
141
+ .option('--json', 'Output as JSON')
142
+ .action((options) => {
131
143
  const lang = getCliLang(process.cwd());
132
144
  const mgr = createManager();
133
145
  if (!mgr.isLoaded()) {
146
+ // B1 契约:--json 时仍输出纯 JSON 单文档
147
+ if (options.json) {
148
+ console.log(JSON.stringify({ schemaVersion: 1, initialized: false }));
149
+ return;
150
+ }
134
151
  console.log(t('common.noInit', lang));
135
152
  return;
136
153
  }
154
+ // B1-5:--json 分支纯 JSON 单文档(不打印任何标题/颜色/提示行)
155
+ if (options.json) {
156
+ console.log(JSON.stringify(buildOverviewJson(mgr), null, 2));
157
+ return;
158
+ }
137
159
  const phase = mgr.getPhase();
138
160
  const current = mgr.getCurrent();
139
161
  const summary = mgr.getSummary();
@@ -254,35 +276,120 @@ export function registerFlowCommand(program) {
254
276
  flow
255
277
  .command('current')
256
278
  .description('显示当前阶段和操作')
257
- .action(() => {
279
+ .option('--json', 'Output as JSON')
280
+ .action((options) => {
258
281
  const lang = getCliLang(process.cwd());
259
282
  const mgr = createManager();
260
283
  if (!mgr.isLoaded()) {
284
+ // B1 契约:--json 时仍输出纯 JSON 单文档(未初始化 → 空串字段)
285
+ if (options.json) {
286
+ console.log(JSON.stringify({ schemaVersion: 1, stage: '', op: '', phase: '' }));
287
+ return;
288
+ }
261
289
  console.log(t('common.noInit', lang));
262
290
  return;
263
291
  }
264
292
  const phase = mgr.getPhase(); // MetaPhase: active/paused/done
265
293
  const current = mgr.getCurrent();
266
- const summary = mgr.getSummary();
267
294
  const data = mgr.getData();
268
- const stagePhase = current?.stage && data?.stages[current.stage]
269
- ? data.stages[current.stage].phase
295
+ // B8:getCurrent() 在 op 为空时返回 null(契约不变);命令层回退读取 pipeline.current 展示 stage + 「(无 op)」
296
+ const rawCurrent = data?.pipeline.current;
297
+ const displayStage = current?.stage ?? rawCurrent?.stage ?? '';
298
+ const displayOp = current?.op ?? rawCurrent?.op ?? '';
299
+ // B1-2:--json 分支纯 JSON 单文档(op 为空串而非 null,便于机器消费)
300
+ if (options.json) {
301
+ console.log(JSON.stringify({ schemaVersion: 1, stage: displayStage, op: displayOp, phase: phase ?? '' }));
302
+ return;
303
+ }
304
+ const summary = mgr.getSummary();
305
+ const stagePhase = displayStage && data?.stages[displayStage]
306
+ ? data.stages[displayStage].phase
270
307
  : t('common.none', lang);
271
308
  console.log(t('flow.current.globalStatus', lang) + `: ${phase}`);
272
- console.log(t('common.stage', lang) + `: ${current?.stage ?? t('common.none', lang)}`);
309
+ // B8:stage 存在但无 op → `{stage}(无 op)`;stage 亦空 → 保持既有「(无)」文案
310
+ if (!displayStage) {
311
+ console.log(t('common.stage', lang) + `: ${t('common.none', lang)}`);
312
+ }
313
+ else if (!displayOp) {
314
+ console.log(t('common.stage', lang) + `: ` + t('flow.current.noOpTmpl', lang, { stage: displayStage }));
315
+ }
316
+ else {
317
+ console.log(t('common.stage', lang) + `: ${displayStage}`);
318
+ }
273
319
  console.log(t('flow.current.stagePhase', lang) + `: ${stagePhase}`);
274
- console.log(t('flow.current.currentOp', lang) + `: ${current ? `${current.stage}.${current.op}` : t('common.none', lang)}`);
320
+ console.log(t('flow.current.currentOp', lang) + `: ${displayOp ? `${displayStage}.${displayOp}` : t('common.none', lang)}`);
275
321
  console.log(t('flow.current.retryCount', lang) + `: ${summary.retryCount}`);
276
322
  });
277
323
  // flow metrics — 展示 Agent 性能指标
278
324
  flow
279
325
  .command('metrics')
280
326
  .description('展示 Agent 性能指标')
281
- .action(() => {
327
+ .option('--json', 'Output as JSON')
328
+ .action((options) => {
282
329
  const lang = getCliLang(process.cwd());
283
330
  const store = MetricsStore.getInstance();
284
331
  store.load();
285
- console.log(store.summary());
332
+ // B1-4:--json 输出结构化指标(顶层对象 + schemaVersion),不输出人类可读表格
333
+ if (options.json) {
334
+ console.log(JSON.stringify({ schemaVersion: 1, ...store.getSummaryData() }, null, 2));
335
+ return;
336
+ }
337
+ console.log(store.summary(lang));
338
+ });
339
+ // flow phases — 自描述:列出全部合法 phase 与运行时转移表
340
+ flow
341
+ .command('phases')
342
+ .description('列出全部合法 phase 及其流转映射(自描述)')
343
+ .option('--json', 'Output { phases, transitions, advanceAccepted, schemaVersion } as JSON')
344
+ .action((options) => {
345
+ const lang = getCliLang(process.cwd());
346
+ const mgr = createManager();
347
+ const phases = mgr.getPipelinePhases();
348
+ const transitions = mgr.getPipelineTransitions();
349
+ // 运行时 vs 内置默认 transitions 差异(T19:使漂移可见,不补组合键)
350
+ const defaults = mgr.getDefaultTransitions();
351
+ const missingKeys = Object.keys(defaults).filter((k) => !(k in transitions));
352
+ const extraKeys = Object.keys(transitions).filter((k) => !(k in defaults));
353
+ const changedKeys = Object.keys(defaults).filter((k) => k in transitions && JSON.stringify(defaults[k]) !== JSON.stringify(transitions[k]));
354
+ const transitionsDiff = { missing: missingKeys, extra: extraKeys, changed: changedKeys };
355
+ if (options.json) {
356
+ // advanceAccepted = 内置 15 phase(flow advance 的推进白名单),与 phases(运行时存在视图,可含自定义)显式区分
357
+ // 既有三字段(phases/transitions/advanceAccepted)+ transitionsDiff 逐字保留;仅追加 schemaVersion:1(B1-6)
358
+ // 注:schemaVersion 未来变更须递增版本号
359
+ console.log(JSON.stringify({ schemaVersion: 1, phases, transitions, advanceAccepted: [...PIPELINE_PHASES], transitionsDiff }, null, 2));
360
+ return;
361
+ }
362
+ console.log(t('flow.phases.title', lang));
363
+ console.log(t('flow.phases.listLabel', lang) + `: ${phases.length}`);
364
+ for (const p of phases) {
365
+ console.log(` - ${p}`);
366
+ }
367
+ console.log('');
368
+ console.log(t('flow.phases.transitionsLabel', lang) + ':');
369
+ for (const [key, targets] of Object.entries(transitions)) {
370
+ console.log(` ${key} → [${targets.join(', ')}]`);
371
+ }
372
+ console.log('');
373
+ // 差异提示(非空时输出)——T19
374
+ if (missingKeys.length + extraKeys.length + changedKeys.length > 0) {
375
+ const parts = [];
376
+ if (missingKeys.length > 0) {
377
+ parts.push(t('flow.phases.transitionsDiffMissing', lang, { keys: missingKeys.join(', ') }));
378
+ }
379
+ if (extraKeys.length > 0) {
380
+ parts.push(t('flow.phases.transitionsDiffExtra', lang, { keys: extraKeys.join(', ') }));
381
+ }
382
+ if (changedKeys.length > 0) {
383
+ parts.push(t('flow.phases.transitionsDiffChanged', lang, { keys: changedKeys.join(', ') }));
384
+ }
385
+ console.log(t('flow.phases.transitionsDiffNote', lang, { detail: parts.join('; ') }));
386
+ }
387
+ // 边界:运行时 pipeline.yaml 含内置 15 phase 之外的 phase 时提示差异(cli/BUG-001 方案 B)
388
+ const customPhases = phases.filter((p) => !PIPELINE_PHASES.includes(p));
389
+ if (customPhases.length > 0) {
390
+ console.log(t('flow.phases.customPhaseNote', lang, { phases: customPhases.join(', ') }));
391
+ }
392
+ console.log(t('flow.phases.hint', lang));
286
393
  });
287
394
  // flow stage — 阶段管理子命令组
288
395
  const stageCmd = flow
@@ -291,10 +398,18 @@ export function registerFlowCommand(program) {
291
398
  // flow stage add <stageId>
292
399
  stageCmd
293
400
  .command('add')
294
- .description('新增流水线阶段')
295
- .argument('<stageId>', '阶段 ID(如 v4.3)')
401
+ .description('新增流水线阶段(仅注册 flow.json,不建目录;通常应使用 openfeel plan stage add)')
402
+ .argument('<stageId>', '阶段 ID(如 v1.1.2-stage-41)')
296
403
  .action((stageId) => {
297
- const lang = getCliLang(process.cwd());
404
+ const projectPath = process.cwd();
405
+ const lang = getCliLang(projectPath);
406
+ // 非法 stageId 统一报错 + 建议名(op-002 校验底座)
407
+ const v = validateStageId(stageId);
408
+ if (!v.ok) {
409
+ console.error(t('common.stageIdInvalidTmpl', lang, { input: stageId }));
410
+ console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, stageId) }));
411
+ process.exit(1);
412
+ }
298
413
  const mgr = createManager();
299
414
  if (!mgr.isLoaded()) {
300
415
  console.error(t('common.errorNoInit', lang));
@@ -306,26 +421,153 @@ export function registerFlowCommand(program) {
306
421
  console.log(t('flow.stage.addedTmpl', lang, { stage: stageId }));
307
422
  }
308
423
  catch (err) {
309
- // 并发冲突:本次未写入 flow.json,输出可重试提示并退出码 2(R3.2)
424
+ // addStage 错误统一处理(并发写冲突 / 目录冲突 / 其他),与 stage create 共用单点(T42)
425
+ handleAddStageError(err, lang);
426
+ }
427
+ });
428
+ // flow stage remove <stageId> [--force] [--dry-run] [--purge]
429
+ stageCmd
430
+ .command('remove <stageId>')
431
+ .description('移除流水线阶段(安全校验;默认仅注销 flow.json,不删目录)')
432
+ .option('--force', '越过安全校验(ops 非空 / 当前活跃阶段 / 被其它阶段依赖)')
433
+ .option('--dry-run', '仅预览,不写盘')
434
+ .option('--purge', '同时删除 plan/{series}/{stageDir}/ 目录(非 TTY 须配 --force)')
435
+ .action(async (stageId, options) => {
436
+ const lang = getCliLang(process.cwd());
437
+ const mgr = createManager();
438
+ if (!mgr.isLoaded()) {
439
+ console.error(t('common.errorNoInit', lang));
440
+ process.exit(1);
441
+ }
442
+ const data = mgr.getData();
443
+ // REV-007:命令层归一化(短名 → 全名),与 stage set / ops list / op-012 advance 同范式
444
+ const stageArg = normalizeStageId(stageId) ?? stageId;
445
+ if (!data?.stages[stageArg]) {
446
+ console.error(t('flow.stage.remove.notFoundTmpl', lang, { stage: stageArg }));
447
+ process.exit(1);
448
+ return;
449
+ }
450
+ // --dry-run:复用 checkRemovable 做安全校验,失败同样 exit 1(REV-005),并呈现引用者(REV-004)
451
+ if (options.dryRun) {
452
+ const info = mgr.checkRemovable(stageArg, { force: options.force });
453
+ console.log(t('flow.stage.remove.dryRunTitle', lang));
454
+ console.log(` ` + t('common.stage', lang) + `: ${stageArg}`);
455
+ console.log(` ops: ${info.opCount}`);
456
+ console.log(` current: ${data.pipeline.current.stage}${info.isCurrent ? ' ←' : ''}`);
457
+ console.log(` ` + t('flow.stage.remove.dryRunReferencing', lang) + `: ` +
458
+ (info.referencing.length > 0 ? info.referencing.join(', ') : t('common.none', lang)));
459
+ if (!info.ok) {
460
+ console.error(t('common.errorTmpl', lang, { msg: info.reason ?? '' }));
461
+ process.exit(1);
462
+ return;
463
+ }
464
+ console.log(t('flow.stage.remove.dryRunOk', lang));
465
+ return;
466
+ }
467
+ // --purge 非 TTY 守卫
468
+ if (options.purge && !options.force && !process.stdout.isTTY) {
469
+ console.error(t('flow.stage.remove.purgeNeedForce', lang));
470
+ process.exit(1);
471
+ return;
472
+ }
473
+ try {
474
+ // --purge TTY 二次确认(--force 跳过)
475
+ if (options.purge && !options.force) {
476
+ const { confirm } = await import('@inquirer/prompts');
477
+ const ok = await confirm({ message: t('flow.stage.remove.purgeConfirm', lang), default: false });
478
+ if (!ok) {
479
+ console.log(t('common.cancelled', lang));
480
+ return;
481
+ }
482
+ }
483
+ // 事务顺序(REV-009):先注销内存注册 → save() 落盘成功 → 再删目录。
484
+ // save() 失败则不会到达目录删除,杜绝「目录已删但注册仍在」的中间态。
485
+ const { purgeTarget } = mgr.removeStage(stageArg, { force: options.force, purge: options.purge });
486
+ mgr.save();
487
+ if (purgeTarget) {
488
+ rmSync(purgeTarget, { recursive: true, force: true });
489
+ console.log(t('flow.stage.remove.purgedTmpl', lang));
490
+ }
491
+ console.log(t('flow.stage.remove.okTmpl', lang, { stage: stageArg }));
492
+ }
493
+ catch (err) {
494
+ // 并发写冲突:统一单点(i18n 文案 + 退出码 2);其余走通用错误
310
495
  if (isFlowConcurrentError(err)) {
311
- console.error(`[并发冲突] flow.json 已被其它进程修改(期望 revision=${err.expectedRevision},磁盘=${err.actualRevision})。`);
312
- console.error('本次修改未写入。请重新执行该命令重试。');
313
- process.exit(2);
496
+ handleConcurrentConflict(err, lang);
314
497
  }
315
498
  const msg = err instanceof Error ? err.message : String(err);
316
499
  console.error(t('common.errorTmpl', lang, { msg }));
317
500
  process.exit(1);
318
501
  }
319
502
  });
503
+ // flow stage set <stageId> [--deps <ids...>]
504
+ stageCmd
505
+ .command('set <stageId>')
506
+ .description('设置阶段依赖(覆盖写入;未指定 --deps 视为清空)')
507
+ .option('--deps <ids...>', '依赖阶段 ID 列表(空格或逗号分隔;含变长参数,建议置于命令末尾)')
508
+ .action((stageId, options) => {
509
+ const projectPath = process.cwd();
510
+ const lang = getCliLang(projectPath);
511
+ // 非法 stageId 统一报错 + 建议名
512
+ const v = validateStageId(stageId);
513
+ if (!v.ok) {
514
+ console.error(t('common.stageIdInvalidTmpl', lang, { input: stageId }));
515
+ console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, stageId) }));
516
+ process.exit(1);
517
+ }
518
+ const mgr = createManager();
519
+ if (!mgr.isLoaded()) {
520
+ console.error(t('common.errorNoInit', lang));
521
+ process.exit(1);
522
+ }
523
+ const data = mgr.getData();
524
+ // 兼容 --deps a,b 与 --deps a b:逐项按逗号再切分、去空(与 plan stage add 对齐)
525
+ const deps = (options.deps ?? [])
526
+ .flatMap((d) => d.split(','))
527
+ .map((s) => s.trim())
528
+ .filter(Boolean);
529
+ // 悬空依赖:命令层强制校验(核心层仅 warn),非法一律 exit 1 且不写盘
530
+ const known = new Set(Object.keys(data.stages).map((k) => normalizeStageId(k) ?? k));
531
+ const invalid = deps.filter((d) => !known.has(normalizeStageId(d) ?? d));
532
+ if (invalid.length > 0) {
533
+ console.error(t('stage.set.depsDanglingTmpl', lang, { deps: invalid.join(', ') }));
534
+ process.exit(1);
535
+ return;
536
+ }
537
+ // 目标阶段必须存在
538
+ const target = normalizeStageId(stageId) ?? stageId;
539
+ if (!data.stages[target]) {
540
+ console.error(t('common.stageIdInvalidTmpl', lang, { input: stageId }));
541
+ process.exit(1);
542
+ return;
543
+ }
544
+ try {
545
+ mgr.setStageDeps(stageId, deps);
546
+ }
547
+ catch (err) {
548
+ if (isFlowConcurrentError(err)) {
549
+ handleConcurrentConflict(err, lang);
550
+ }
551
+ const msg = err instanceof Error ? err.message : String(err);
552
+ console.error(t('common.errorTmpl', lang, { msg }));
553
+ process.exit(1);
554
+ return;
555
+ }
556
+ console.log(t('stage.set.depsOkTmpl', lang, {
557
+ stage: target,
558
+ deps: deps.length > 0 ? deps.join(', ') : t('common.none', lang),
559
+ }));
560
+ });
320
561
  // flow advance --stage <id> --to <phase> [--op <id>] [--force]
321
562
  flow
322
563
  .command('advance')
323
564
  .description('推进流水线阶段')
324
565
  .option('--op <id>', '操作 ID(如 stage-01.op-001),仅用于日志/展示')
325
- .requiredOption('--to <phase>', '目标阶段(如 exec_running)')
566
+ .requiredOption('--to <phase>', '目标阶段(如 exec_running)。合法 phase 与转移表:openfeel flow phases')
326
567
  .option('--stage <id>', '阶段 ID(如 stage-03),必须指定')
327
568
  .option('--force', '强制执行(跳过非法 phase 校验和阶段跳跃检查,但不可绕过 REV 阻塞检查)')
328
569
  .option('--dry-run', '仅验证不执行修改(预览输出)。与 --force 组合时跳过校验但仍不执行修改')
570
+ .option('--quiet', '静默非错误输出(成功确认与 Git 警告均不打印)')
329
571
  .action((options) => {
330
572
  const lang = getCliLang(process.cwd());
331
573
  // 自定义 --stage 必选校验(提供中文错误提示)
@@ -338,12 +580,19 @@ export function registerFlowCommand(program) {
338
580
  console.error(t('common.errorNoInit', lang));
339
581
  process.exit(1);
340
582
  }
583
+ // REV-005:命令层归一化(短名 → 全名),与 plan.ts / flow.ts 其它 stage 解析点同范式
584
+ const stageArg = normalizeStageId(options.stage) ?? options.stage;
341
585
  // 自动修复 phase/status 不一致(在 validate() 前执行)
342
586
  if (options.stage) {
343
- const repairResult = mgr.autoRepairInconsistency(options.stage);
587
+ // B1 修复:dry-run 走预览模式(不写内存/不写盘),仅在非 dry-run 时 save()
588
+ const repairResult = mgr.autoRepairInconsistency(stageArg, { dryRun: options.dryRun });
344
589
  if (repairResult.fixed) {
345
- console.log(t('flow.advance.autoRepaired', lang) + `: ${repairResult.detail}`);
346
- mgr.save();
590
+ console.log(options.dryRun
591
+ ? t('flow.advance.autoRepairPreview', lang) + `: ${repairResult.detail}`
592
+ : t('flow.advance.autoRepaired', lang) + `: ${repairResult.detail}`);
593
+ if (!options.dryRun) {
594
+ mgr.save();
595
+ }
347
596
  }
348
597
  }
349
598
  const { valid, errors, warnings } = mgr.validate();
@@ -371,15 +620,32 @@ export function registerFlowCommand(program) {
371
620
  process.exit(1);
372
621
  }
373
622
  }
374
- // 阶段跳跃保护:基于 stage phase 检查当前 phase 到目标 phase 是否存在直接路径
623
+ // 阶段路径解析(B5-1):非 --force 时求「当前 phase → to」的唯一可达路径
624
+ // - 唯一路径(含单步)→ 沿路径逐步推进;单步时与既有行为完全一致
625
+ // - 无路径 / 多义 / 超深 → 拒绝(exit 1 + 可达目标;保留既有跳转诊断)
626
+ // - 已在 to → no-op 成功
627
+ let phasePath = [options.to];
375
628
  if (!options.force) {
376
- const phaseResult = PipelinePhaseSchema.safeParse(options.to);
377
- if (phaseResult.success && !mgr.hasTransition(options.to, options.stage)) {
378
- // 获取当前 phase 和合法目标列表(增强诊断信息)
629
+ const pathResult = mgr.findPhasePath(stageArg, options.to);
630
+ if (pathResult.reason === 'no-path' || pathResult.reason === 'ambiguous' || pathResult.reason === 'depth-exceeded') {
379
631
  const data = mgr.getData();
380
- const currentPhase = data?.stages[options.stage || '']?.phase ?? t('common.unknown', lang);
381
- const availableTargets = mgr.getAvailablePhases(options.stage);
382
- console.error(t('flow.advance.errorPhaseJumpTmpl', lang, { stage: options.stage || '', to: options.to }));
632
+ const currentPhase = data?.stages[stageArg]?.phase ?? t('common.unknown', lang);
633
+ const availableTargets = mgr.getAvailablePhases(stageArg);
634
+ if (pathResult.reason === 'ambiguous') {
635
+ console.error(t('flow.advance.ambiguousTmpl', lang));
636
+ }
637
+ else if (pathResult.reason === 'depth-exceeded') {
638
+ console.error(t('flow.advance.depthExceededTmpl', lang, { max: String(PHASE_PATH_MAX_DEPTH) }));
639
+ }
640
+ else {
641
+ console.error(t('flow.advance.noPathTmpl', lang, {
642
+ from: currentPhase,
643
+ to: options.to,
644
+ targets: availableTargets.length > 0 ? `[${availableTargets.join(', ')}]` : t('common.none', lang),
645
+ }));
646
+ }
647
+ // 保留既有跳转诊断(可达目标)——与现状一致
648
+ console.error(t('flow.advance.errorPhaseJumpTmpl', lang, { stage: stageArg, to: options.to }));
383
649
  console.error(t('flow.advance.currentPhaseTmpl', lang, { phase: currentPhase }));
384
650
  if (availableTargets.length > 0) {
385
651
  console.error(t('flow.advance.availableTargets', lang) + `: [${availableTargets.join(', ')}]`);
@@ -389,83 +655,132 @@ export function registerFlowCommand(program) {
389
655
  }
390
656
  console.error(t('flow.advance.hintUseForce', lang));
391
657
  process.exit(1);
658
+ return;
392
659
  }
660
+ if (pathResult.reason === 'already-at-target') {
661
+ // 已在目标 phase → no-op 成功
662
+ if (!options.quiet) {
663
+ console.log(t('flow.advance.alreadyAtTargetTmpl', lang, { stage: stageArg, to: options.to }));
664
+ }
665
+ return;
666
+ }
667
+ phasePath = pathResult.path;
393
668
  }
394
- // 安全提示:跳过审查直接 done
669
+ // 安全提示:跳过审查直接 done(保持既有条件)
395
670
  const SKIP_WARN_PHASES = ['exec_running', 'review_pending'];
396
671
  if (options.to === 'done' && options.stage) {
397
- const stage = (mgr.getData()?.stages || {})[options.stage];
672
+ const stage = (mgr.getData()?.stages || {})[stageArg];
398
673
  if (stage && SKIP_WARN_PHASES.includes(stage.phase)) {
399
674
  console.warn(t('flow.advance.warnSkipReview', lang));
400
675
  }
401
676
  }
402
- // REV 闭环(命令层兜底):推进到 done 时检查 blocking REV
677
+ // REV 闭环(命令层兜底):推进到 done 时检查 blocking REV(单步入口;多步循环每步后另复检,B5-3)
403
678
  if (options.to === 'done' && options.stage) {
404
- const allReviews = mgr.getReviewItems();
405
- const stageReviews = allReviews.filter((r) => r.op.startsWith(options.stage) || r.op === options.stage);
406
- const blockingOpen = stageReviews.filter((r) => r.blocking !== false && r.status === 'open');
679
+ const blockingOpen = assertNoBlockingOpenRev(mgr, stageArg, lang);
407
680
  if (blockingOpen.length > 0) {
408
- console.warn(`[!] 检测到 ${blockingOpen.length} 个未解决的阻塞 REV:`);
409
- for (const rev of blockingOpen) {
410
- console.warn(` ${rev.id}: ${rev.title} (priority=${rev.priority})`);
411
- }
412
681
  if (options.force) {
413
- console.warn('[!] --force 已指定,但 REV 安全检查不可绕过。拒绝推进。');
682
+ console.warn(t('flow.advance.forceRevRefused', lang));
414
683
  }
415
- console.error('错误:blocking REV 未解决前禁止推进到 done。');
416
- console.error('请先解决上述 REV 或通过 flow review resolve 标记为非阻塞。');
684
+ console.error(t('flow.advance.blockingRevRefused', lang));
685
+ console.error(t('flow.advance.blockingRevHint', lang));
417
686
  process.exit(1);
687
+ return;
418
688
  }
419
689
  }
420
- // --dry-run:仅验证合法性,不实际修改 flow.json
690
+ // 起始 phase(供多步逐步输出);REV-005/REV-006:以归一化后的 stageArg 索引
691
+ const startPhase = mgr.getData()?.stages[stageArg]?.phase ?? t('common.unknown', lang);
692
+ // --dry-run:仅验证合法性,不实际修改 flow.json(B5-2:多步时打印完整路径)
421
693
  if (options.dryRun) {
422
- const data = mgr.getData();
423
- const fromPhase = data?.stages[options.stage]?.phase ?? t('common.unknown', lang);
424
- const toPhase = options.to;
425
694
  if (options.force) {
426
695
  console.warn(t('flow.advance.dryRunForceWarn', lang));
427
696
  }
428
697
  console.log(t('flow.advance.dryRunTitle', lang));
429
- console.log(` ` + t('common.stage', lang) + `: ${options.stage}`);
430
- console.log(` ` + t('flow.advance.dryRunFrom', lang) + `: ${fromPhase}`);
431
- console.log(` ` + t('flow.advance.dryRunTo', lang) + `: ${toPhase}`);
698
+ console.log(` ` + t('common.stage', lang) + `: ${stageArg}`);
699
+ console.log(` ` + t('flow.advance.dryRunFrom', lang) + `: ${startPhase}`);
700
+ console.log(` ` + t('flow.advance.dryRunTo', lang) + `: ${options.to}`);
701
+ if (phasePath.length > 1) {
702
+ const fullPath = [startPhase, ...phasePath].join(t('flow.advance.pathArrow', lang));
703
+ console.log(` ` + t('flow.advance.pathTitle', lang) + ` ${fullPath}`);
704
+ }
432
705
  console.log('');
433
706
  console.log(t('flow.advance.dryRunOk', lang));
434
707
  return;
435
708
  }
436
- let archived = false;
437
- try {
438
- archived = mgr.advanceStagePhase(options.stage, options.to, 'cli');
709
+ // 逐步推进(B5-2 / A3):唯一路径 → 依次调用 advanceStagePhase(保留每步校验/日志/checkpoint)
710
+ // 中间态不回滚(A3);失败 → 输出「已完成 / 失败点 / 剩余路径」+ exit 1
711
+ const totalSteps = phasePath.length;
712
+ let archivedAny = false;
713
+ for (let i = 0; i < totalSteps; i++) {
714
+ const next = phasePath[i];
715
+ let archived = false;
716
+ try {
717
+ archived = mgr.advanceStagePhase(stageArg, next, 'cli');
718
+ }
719
+ catch (err) {
720
+ if (totalSteps > 1) {
721
+ const remaining = phasePath.slice(i).join(t('flow.advance.pathArrow', lang));
722
+ const doneTo = i > 0 ? phasePath[i - 1] : t('common.none', lang);
723
+ console.error(t('flow.advance.partialFailTmpl', lang, { done: doneTo, failed: next, remaining }));
724
+ }
725
+ const msg = err instanceof Error ? err.message : String(err);
726
+ console.error(t('common.errorTmpl', lang, { msg }));
727
+ process.exit(1);
728
+ return;
729
+ }
730
+ archivedAny = archivedAny || archived;
731
+ mgr.save();
732
+ // 归档 commit 必须在 flow.json save 之后执行,确保 commit 包含本次 phase 变更
733
+ if (archived) {
734
+ mgr.autoCommitOnDone(stageArg);
735
+ }
736
+ // 多步时逐步输出(单步保持既有静默,不新增输出)
737
+ if (totalSteps > 1 && !options.quiet) {
738
+ console.log(t('flow.advance.stepOkTmpl', lang, {
739
+ stage: stageArg,
740
+ from: i > 0 ? phasePath[i - 1] : startPhase,
741
+ to: next,
742
+ }));
743
+ }
744
+ // B5-3(REV-52-001):多步路径每步完成后复检 blocking open REV(至少进入 done 前一次)
745
+ // 等价论证:addAutoFixReview 强制 status='resolved'(恒非 blocking open)且 advance 路径不创建 review,
746
+ // 故此复检为**防御性加固**(防未来语义漂移)。
747
+ if (totalSteps > 1) {
748
+ const blockingOpen = assertNoBlockingOpenRev(mgr, stageArg, lang);
749
+ if (blockingOpen.length > 0) {
750
+ const remainingPath = phasePath.slice(i + 1);
751
+ console.error(t('flow.advance.blockedByRevTmpl', lang, {
752
+ revs: blockingOpen.map((r) => r.id).join(', '),
753
+ remaining: remainingPath.length > 0 ? remainingPath.join(t('flow.advance.pathArrow', lang)) : t('common.none', lang),
754
+ }));
755
+ process.exit(1);
756
+ return;
757
+ }
758
+ }
439
759
  }
440
- catch (err) {
441
- const msg = err instanceof Error ? err.message : String(err);
442
- console.error(t('common.errorTmpl', lang, { msg }));
443
- process.exit(1);
760
+ // 成功确认(单步与多步统一);--quiet 完全静默
761
+ if (!options.quiet) {
762
+ console.log(t('flow.advance.okTmpl', lang, { stage: stageArg, to: options.to }));
444
763
  }
445
- mgr.save();
446
- // 归档 commit 必须在 flow.json save 之后执行,确保 commit 包含本次 phase 变更
447
- if (archived) {
448
- mgr.autoCommitOnDone(options.stage);
764
+ // git 脏区检查(安全网):默认仅 --to done 时提示;--quiet / 非 done → 完全跳过(含跳过 git 子进程)
765
+ if (!options.quiet && (options.to === 'done' || archivedAny)) {
766
+ try {
767
+ const gitStatus = execSync('git status --porcelain', {
768
+ cwd: process.cwd(),
769
+ encoding: 'utf-8',
770
+ timeout: 5000,
771
+ }).trim();
772
+ if (gitStatus) {
773
+ console.warn(t('flow.advance.gitDirtyBoxTop', lang));
774
+ console.warn(t('flow.advance.gitDirtyBoxMsg', lang));
775
+ console.warn(t('flow.advance.gitDirtyBoxHint', lang));
776
+ console.warn(t('flow.advance.gitDirtyBoxBottom', lang));
777
+ }
778
+ }
779
+ catch {
780
+ // git 不可用(无 .git 目录或 git 未安装)时静默跳过
781
+ }
449
782
  }
450
- console.log(t('flow.advance.okTmpl', lang, { stage: options.stage || '', to: options.to }));
451
- // git 脏区检查(安全网):openfeel-executor 未提交时输出醒目警告
452
- try {
453
- const gitStatus = execSync('git status --porcelain', {
454
- cwd: process.cwd(),
455
- encoding: 'utf-8',
456
- timeout: 5000,
457
- }).trim();
458
- if (gitStatus) {
459
- console.warn('[!] ╔════════════════════════════════════════╗');
460
- console.warn('[!] ║ ⚠ Git 脏区警告:存在未提交的变更 ║');
461
- console.warn('[!] ║ 请确认 Executor 已完成 git commit ║');
462
- console.warn('[!] ╚════════════════════════════════════════╝');
463
- }
464
- }
465
- catch {
466
- // git 不可用(无 .git 目录或 git 未安装)时静默跳过
467
- }
468
- if (options.op) {
783
+ if (options.op && !options.quiet) {
469
784
  console.log(t('flow.advance.opLabelTmpl', lang, { op: options.op }));
470
785
  }
471
786
  });
@@ -486,6 +801,15 @@ export function registerFlowCommand(program) {
486
801
  console.error(t('flow.attempt.errorInvalidResult', lang));
487
802
  process.exit(1);
488
803
  }
804
+ // B4-5(A1 第 5 条)/ REV-52-002:draft op 拒绝记录执行结果(命令层守卫;核心层 recordAttempt 另有兜底)
805
+ if (mgr.getOpState(options.op) === 'draft') {
806
+ const dotIdx = options.op.lastIndexOf('.');
807
+ const stageId = dotIdx >= 0 ? options.op.substring(0, dotIdx) : '';
808
+ const localOpId = dotIdx >= 0 ? options.op.substring(dotIdx + 1) : options.op;
809
+ console.error(t('flow.attempt.draftRefusedTmpl', lang, { stage: stageId, opId: localOpId }));
810
+ process.exit(1);
811
+ return;
812
+ }
489
813
  const outcome = mgr.recordAttempt(options.op, options.result);
490
814
  mgr.save();
491
815
  if (options.result === 'pass') {
@@ -501,6 +825,92 @@ export function registerFlowCommand(program) {
501
825
  mgr.save();
502
826
  console.log(t('flow.attempt.autoReplan', lang));
503
827
  }
828
+ // N4-2:输出当前指针(只读,与 flow current 同源;attempt 后 current.op 已由 syncCurrentOp 同步)
829
+ const cur = mgr.getData()?.pipeline.current;
830
+ if (cur && cur.stage) {
831
+ if (cur.op) {
832
+ console.log(t('flow.attempt.currentOpTmpl', lang, { stage: cur.stage, op: cur.op }));
833
+ }
834
+ else {
835
+ console.log(t('flow.attempt.currentOpEmptyTmpl', lang, { stage: cur.stage }));
836
+ }
837
+ }
838
+ });
839
+ // flow ops list [--stage <id>] [--json] — B3-1:操作方案视图(状态 + 模板填充度 + draft 分组)
840
+ const opsCmd = flow
841
+ .command('ops')
842
+ .description('操作方案视图');
843
+ opsCmd
844
+ .command('list')
845
+ .description('列出操作方案(含状态与模板填充度)')
846
+ .option('--stage <id>', '仅列出指定阶段')
847
+ .option('--json', 'Output as JSON')
848
+ .action((options) => {
849
+ const lang = getCliLang(process.cwd());
850
+ const mgr = createManager();
851
+ if (!mgr.isLoaded()) {
852
+ if (options.json) {
853
+ console.log(JSON.stringify({ schemaVersion: 1, ops: [] }));
854
+ return;
855
+ }
856
+ console.log(t('common.noInit', lang));
857
+ return;
858
+ }
859
+ const filterStage = options.stage ? (normalizeStageId(options.stage) ?? options.stage) : undefined;
860
+ const data = mgr.getData();
861
+ const items = [];
862
+ for (const [stageId, stage] of Object.entries(data.stages)) {
863
+ if (filterStage && stageId !== filterStage) {
864
+ continue;
865
+ }
866
+ const opsMap = stage.ops && typeof stage.ops === 'object' && !Array.isArray(stage.ops) ? stage.ops : {};
867
+ for (const [opId, op] of Object.entries(opsMap)) {
868
+ // 填充度:复用 flow-manager 的单一标记判定。
869
+ // 已知边界(stage-54 E1-补 / A9):content 为 null(op 文件缺失 / 命名不含前缀 / 目录不可读)时视为 filled,
870
+ // 与 health 的「null 跳过不报」为同向漏检(非分叉);现行数据短名命中故不可复现,
871
+ // 不扩展 detectFillState 返回域与 --json 契约。
872
+ const content = mgr.readOpTemplate(stageId, opId);
873
+ const fill = content === null ? 'filled' : detectFillState(content);
874
+ items.push({
875
+ stage: stageId,
876
+ opId,
877
+ title: op.title ?? '',
878
+ state: op.state ?? '',
879
+ fill,
880
+ });
881
+ }
882
+ }
883
+ if (options.json) {
884
+ // 复用 op-001 口径:顶层对象 + schemaVersion:1
885
+ const ops = items.map((it) => ({
886
+ stage: it.stage,
887
+ opId: it.opId,
888
+ title: it.title,
889
+ state: it.state,
890
+ fill: it.fill,
891
+ ...(it.fill === 'empty' ? { warning: t('flow.ops.emptyWarningTmpl', lang, { opId: it.opId }) } : {}),
892
+ }));
893
+ console.log(JSON.stringify({ schemaVersion: 1, ops }, null, 2));
894
+ return;
895
+ }
896
+ console.log(t('flow.ops.title', lang));
897
+ const published = items.filter((i) => i.state !== 'draft');
898
+ const drafts = items.filter((i) => i.state === 'draft');
899
+ const printGroup = (label, list) => {
900
+ if (list.length === 0) {
901
+ return;
902
+ }
903
+ console.log(label);
904
+ for (const it of list) {
905
+ console.log(` ${it.stage}.${it.opId} [${it.state}] (${it.fill}) ${it.title}`);
906
+ // 空模板 warning(draft 亦提示,引导 publish 前先填充)
907
+ if (it.fill === 'empty') {
908
+ console.log(t('flow.ops.emptyWarningTmpl', lang, { opId: it.opId }));
909
+ }
910
+ }
911
+ };
912
+ printGroup(t('flow.ops.groupActive', lang), published);
913
+ printGroup(t('flow.ops.groupDraft', lang), drafts);
504
914
  });
505
915
  // flow log [--last <n>]
506
916
  flow
@@ -545,60 +955,49 @@ export function registerFlowCommand(program) {
545
955
  .option('--blocking [boolean]', '是否阻塞流水线(默认 true)', 'true')
546
956
  .action((options) => {
547
957
  const lang = getCliLang(process.cwd());
548
- const mgr = createManager();
549
- if (!mgr.isLoaded()) {
550
- console.error(t('common.errorNoInit', lang));
551
- process.exit(1);
552
- }
553
- // 自动生成 REV ID(基于已有审查条目数量递增)
554
- const existingReviews = mgr.getReviewItems();
555
- const revId = `REV-${String(existingReviews.length + 1).padStart(3, '0')}`;
556
- const reviewItem = {
557
- id: revId,
558
- op: options.op,
559
- status: 'open',
560
- priority: 'medium',
958
+ // 单点实现:解析 / 校验 / REV ID 分配 / 写入均由 addReviewEntry 统一处理(T37/R4)
959
+ const blocking = options.blocking !== undefined
960
+ ? !(options.blocking === 'false' || options.blocking === false)
961
+ : true;
962
+ const result = addReviewEntry(process.cwd(), {
963
+ opId: options.op,
561
964
  title: options.title || t('flow.review.detail', lang) + `: ${options.op}`,
562
- filed_by: 'cli',
563
- filed_at: new Date().toISOString(),
564
- canAutoFix: !!options.autoFix,
965
+ priority: 'medium',
565
966
  autoFixDetail: options.autoFix,
566
- blocking: options.blocking !== undefined ? (options.blocking === 'false' || options.blocking === false ? false : true) : true,
567
- };
568
- if (options.autoFix) {
569
- // opId 格式校验(来自 REV-011:命令层校验,避免无效 opId 穿透到 addAutoFixReview)
570
- const dotIdx = options.op.lastIndexOf('.');
571
- if (dotIdx === -1) {
572
- console.error(t('common.invalidOpId', lang));
573
- process.exit(1);
967
+ blocking,
968
+ });
969
+ if (result.error) {
970
+ // 错误路径:按错误码分流 i18n 文案(单点返回 code,命令层只负责呈现)
971
+ switch (result.error.code) {
972
+ case 'notLoaded':
973
+ console.error(t('common.errorNoInit', lang));
974
+ break;
975
+ case 'invalidOpId':
976
+ console.error(t('common.invalidOpId', lang));
977
+ break;
978
+ case 'stageNotFound':
979
+ console.error(t('flow.review.errorStageNotFoundTmpl', lang, { opId: result.error.opId, stage: result.error.stage }));
980
+ break;
981
+ case 'opNotFound':
982
+ console.error(t('flow.review.errorOpNotFoundTmpl', lang, { opId: result.error.opId, op: result.error.op, stage: result.error.stage }));
983
+ break;
984
+ case 'invalidPriority':
985
+ console.error(t('flow.review.errorInvalidPriorityTmpl', lang, { priority: result.error.priority }));
986
+ break;
574
987
  }
575
- const stageId = options.op.substring(0, dotIdx);
576
- const opLocalId = options.op.substring(dotIdx + 1);
577
- // 校验 stage 是否存在于 flow.json
578
- const data = mgr.getData();
579
- if (!data || !data.stages[stageId]) {
580
- console.error(t('flow.review.errorStageNotFoundTmpl', lang, { opId: options.op, stage: stageId }));
581
- process.exit(1);
582
- }
583
- // 校验 op 是否存在于对应 stage 中
584
- if (!data.stages[stageId].ops[opLocalId]) {
585
- console.error(t('flow.review.errorOpNotFoundTmpl', lang, { opId: options.op, op: opLocalId, stage: stageId }));
586
- process.exit(1);
587
- }
588
- // 自动修复路径:记录 REV 条目(状态直接 resolved),跳过 review_failed → scheme_pending
589
- mgr.addAutoFixReview(reviewItem, options.op);
590
- mgr.save();
591
- const blockingLabel = reviewItem.blocking !== false ? t('flow.review.labelBlocking', lang) : t('flow.review.labelNonBlocking', lang);
592
- console.log(t('flow.review.addedAutoFixTmpl', lang, { label: blockingLabel, revId }));
988
+ process.exit(1);
989
+ return;
990
+ }
991
+ const review = result.review;
992
+ const blockingLabel = review.blocking !== false ? t('flow.review.labelBlocking', lang) : t('flow.review.labelNonBlocking', lang);
993
+ if (options.autoFix) {
994
+ console.log(t('flow.review.addedAutoFixTmpl', lang, { label: blockingLabel, revId: review.id }));
593
995
  console.log(` ` + t('common.op', lang) + `: ${options.op}`);
594
996
  console.log(` ` + t('flow.review.detail', lang) + `: ${options.autoFix}`);
595
997
  console.log(t('flow.review.autoFixPhase', lang));
596
998
  }
597
999
  else {
598
- mgr.addReview(reviewItem);
599
- mgr.save();
600
- const blockingLabel = reviewItem.blocking !== false ? t('flow.review.labelBlocking', lang) : t('flow.review.labelNonBlocking', lang);
601
- console.log(t('flow.review.addedTmpl', lang, { label: blockingLabel, revId, op: options.op }) + (options.title ? ` — ${options.title}` : ''));
1000
+ console.log(t('flow.review.addedTmpl', lang, { label: blockingLabel, revId: review.id, op: options.op }) + (options.title ? ` — ${options.title}` : ''));
602
1001
  }
603
1002
  });
604
1003
  // flow review resolve <rev-id>
@@ -623,6 +1022,86 @@ export function registerFlowCommand(program) {
623
1022
  process.exit(1);
624
1023
  }
625
1024
  });
1025
+ // flow review update <rev-id> [--priority] [--title] [--blocking]
1026
+ reviewCmd
1027
+ .command('update <rev-id>')
1028
+ .description('更新审查条目字段(至少提供一个选项)')
1029
+ .option('--priority <level>', '优先级(high/medium/low)')
1030
+ .option('--title <text>', '审查标题')
1031
+ .option('--blocking <true|false>', '是否阻塞流水线(true/false)')
1032
+ .action((revId, options) => {
1033
+ const lang = getCliLang(process.cwd());
1034
+ const mgr = createManager();
1035
+ if (!mgr.isLoaded()) {
1036
+ console.error(t('common.errorNoInit', lang));
1037
+ process.exit(1);
1038
+ }
1039
+ const patch = {};
1040
+ let hasField = false;
1041
+ if (options.priority !== undefined) {
1042
+ // 枚举校验:非法优先级 exit 1
1043
+ if (options.priority !== 'high' && options.priority !== 'medium' && options.priority !== 'low') {
1044
+ console.error(t('flow.review.errorInvalidPriorityTmpl', lang, { priority: options.priority }));
1045
+ process.exit(1);
1046
+ return;
1047
+ }
1048
+ patch.priority = options.priority;
1049
+ hasField = true;
1050
+ }
1051
+ if (options.title !== undefined) {
1052
+ patch.title = options.title;
1053
+ hasField = true;
1054
+ }
1055
+ if (options.blocking !== undefined) {
1056
+ // 布尔解析:非法值 exit 1
1057
+ if (options.blocking !== 'true' && options.blocking !== 'false') {
1058
+ console.error(t('flow.review.invalidBlockingTmpl', lang, { value: options.blocking }));
1059
+ process.exit(1);
1060
+ return;
1061
+ }
1062
+ patch.blocking = options.blocking === 'true';
1063
+ hasField = true;
1064
+ }
1065
+ // 无字段可更新:exit 1
1066
+ if (!hasField) {
1067
+ console.error(t('flow.review.updateNoFieldTmpl', lang));
1068
+ process.exit(1);
1069
+ return;
1070
+ }
1071
+ const ok = mgr.updateReview(revId, patch);
1072
+ if (!ok) {
1073
+ console.error(t('flow.review.notFoundTmpl', lang, { revId }));
1074
+ process.exit(1);
1075
+ return;
1076
+ }
1077
+ console.log(t('flow.review.updateOkTmpl', lang, { revId }));
1078
+ });
1079
+ // flow review remove <rev-id>
1080
+ reviewCmd
1081
+ .command('remove <rev-id>')
1082
+ .description('删除审查条目(留审计日志;打印被删条目标题)')
1083
+ .action((revId) => {
1084
+ const lang = getCliLang(process.cwd());
1085
+ const mgr = createManager();
1086
+ if (!mgr.isLoaded()) {
1087
+ console.error(t('common.errorNoInit', lang));
1088
+ process.exit(1);
1089
+ }
1090
+ // 先取标题供追溯(删除后不可得)
1091
+ const item = mgr.getReviewItems().find((r) => r.id === revId);
1092
+ if (!item) {
1093
+ console.error(t('flow.review.notFoundTmpl', lang, { revId }));
1094
+ process.exit(1);
1095
+ return;
1096
+ }
1097
+ const ok = mgr.removeReview(revId);
1098
+ if (!ok) {
1099
+ console.error(t('flow.review.notFoundTmpl', lang, { revId }));
1100
+ process.exit(1);
1101
+ return;
1102
+ }
1103
+ console.log(t('flow.review.removeOkTmpl', lang, { revId, title: item.title }));
1104
+ });
626
1105
  // flow retry --op <id>
627
1106
  flow
628
1107
  .command('retry')
@@ -647,9 +1126,11 @@ export function registerFlowCommand(program) {
647
1126
  console.error(t('common.invalidOpId', lang));
648
1127
  process.exit(1);
649
1128
  }
650
- const stageId = options.op.substring(0, dotIdx);
1129
+ const rawStageId = options.op.substring(0, dotIdx);
651
1130
  const opLocalId = options.op.substring(dotIdx + 1);
652
- const stage = data.stages[stageId];
1131
+ // REV-007:短名前缀归一化(与 op-012 advance 同范式)+ 双键回退(归一化键优先)
1132
+ const stageId = normalizeStageId(rawStageId) ?? rawStageId;
1133
+ const stage = data.stages[stageId] ?? data.stages[rawStageId];
653
1134
  if (!stage) {
654
1135
  console.error(t('flow.retry.errorStageNotFoundTmpl', lang, { stage: stageId }));
655
1136
  process.exit(1);
@@ -673,9 +1154,11 @@ export function registerFlowCommand(program) {
673
1154
  .description('自动检测并修复 flow.json 中的常见问题')
674
1155
  .option('--dry-run', '仅检测不修复')
675
1156
  .option('--backup', '修复前备份为 .bak')
1157
+ .option('--prune-orphans', '清理键孤儿(flow.json 中已注册但 ops/ 无文件的 op 键;不删除文件)')
676
1158
  .action((options) => {
677
1159
  const lang = getCliLang(process.cwd());
678
- const mgr = new FlowManager(process.cwd());
1160
+ // 复用统一构造入口 createManager(),避免第二处 new FlowManager(T13)
1161
+ const mgr = createManager();
679
1162
  // --backup 选项:修复前手动备份
680
1163
  if (options.backup) {
681
1164
  const fp = resolve(process.cwd(), '.openfeel', 'flow.json');
@@ -687,7 +1170,7 @@ export function registerFlowCommand(program) {
687
1170
  console.log(t('flow.repair.noBackup', lang));
688
1171
  }
689
1172
  }
690
- const result = mgr.repair(options.dryRun ?? false);
1173
+ const result = mgr.repair(options.dryRun ?? false, { pruneOrphans: options.pruneOrphans ?? false });
691
1174
  if (options.dryRun) {
692
1175
  console.log(t('flow.repair.dryRunTitle', lang));
693
1176
  }
@@ -723,6 +1206,29 @@ export function registerFlowCommand(program) {
723
1206
  console.log(' openfeel flow migrate');
724
1207
  console.log(' ' + t('flow.repair.migrationPreview', lang));
725
1208
  }
1209
+ // N1-2:孤儿 op 对账报告(A2:默认只报告,不删任何条目)
1210
+ const orphans = result.orphans;
1211
+ if (orphans && orphans.keyOrphans.length + orphans.fileOrphans.length > 0) {
1212
+ console.log('\n' + t('flow.repair.orphansTitle', lang));
1213
+ if (orphans.keyOrphans.length > 0) {
1214
+ console.log(t('flow.repair.orphanKeyTmpl', lang, {
1215
+ items: orphans.keyOrphans.map((o) => `${o.stage}.${o.opId}`).join(', '),
1216
+ }));
1217
+ }
1218
+ if (orphans.fileOrphans.length > 0) {
1219
+ // L7-1:文件孤儿只读统计(条数 + 前 5 条清单 + 说明);按 A6 不提供清理入口
1220
+ console.log(t('flow.repair.fileOrphanTitle', lang) + ` ${orphans.fileOrphans.length}`);
1221
+ const sample = orphans.fileOrphans.slice(0, 5).map((o) => `${o.stage}.${o.opId}`).join(', ');
1222
+ console.log(t('flow.repair.orphanFileTmpl', lang, {
1223
+ items: sample + (orphans.fileOrphans.length > 5 ? ' …' : ''),
1224
+ }));
1225
+ console.log(t('flow.repair.fileOrphanNote', lang));
1226
+ }
1227
+ // --prune-orphans:仅清理 keyOrphans(正式执行才输出)
1228
+ if (options.pruneOrphans && !options.dryRun && orphans.keyOrphans.length > 0) {
1229
+ console.log(t('flow.repair.pruneOkTmpl', lang, { n: String(orphans.keyOrphans.length) }));
1230
+ }
1231
+ }
726
1232
  });
727
1233
  // flow migrate [--dry-run] [--no-backup]
728
1234
  flow
@@ -830,9 +1336,65 @@ export function registerFlowCommand(program) {
830
1336
  .command('health')
831
1337
  .description('全面健康检查 flow.json / 跨文件一致性 / 僵尸状态 / config.yaml 等')
832
1338
  .option('--quick', '仅检查关键项(phase/current 合法性,跳过其他检查)')
1339
+ .option('--json', 'Output as JSON')
1340
+ .option('--fix', 'Reconcile status.md "Status" field against flow.json (that field only)')
1341
+ .option('--dry-run', 'Preview only (combine with --fix); nothing is written')
833
1342
  .action((options) => {
834
1343
  const lang = getCliLang(process.cwd());
835
1344
  const mgr = createManager();
1345
+ // B2-2:--fix 状态对账(仅回写 status.md「状态」字段;--dry-run 预览不写盘)
1346
+ let reconciled;
1347
+ if (options.fix) {
1348
+ try {
1349
+ reconciled = mgr.reconcileStatusMd({ dryRun: options.dryRun });
1350
+ }
1351
+ catch (err) {
1352
+ // 写盘失败 → 明确错误 + 非 0 退出
1353
+ const msg = err instanceof Error ? err.message : String(err);
1354
+ console.error(t('common.errorTmpl', lang, { msg }));
1355
+ process.exit(1);
1356
+ return;
1357
+ }
1358
+ }
1359
+ // B1-3:--json 分支纯 JSON 单文档;退出码语义不变(有 fail → 非 0)
1360
+ if (options.json) {
1361
+ const report = mgr.getHealthReport(options.quick ?? false);
1362
+ const payload = { schemaVersion: 1, ok: report.ok, items: report.items };
1363
+ // 与 op-001 的 health --json 合并字段(--fix 时追加 reconciled)
1364
+ if (reconciled) {
1365
+ payload.reconciled = reconciled;
1366
+ }
1367
+ console.log(JSON.stringify(payload, null, 2));
1368
+ if (!report.ok) {
1369
+ process.exit(1);
1370
+ }
1371
+ return;
1372
+ }
1373
+ // 人类可读:先打印对账结果(仅 --fix 时)
1374
+ if (reconciled) {
1375
+ console.log(t('flow.health.fixTitle', lang));
1376
+ for (const item of reconciled) {
1377
+ console.log(t('flow.health.fixItemTmpl', lang, {
1378
+ stage: item.stage,
1379
+ from: item.from || '-',
1380
+ to: item.to,
1381
+ result: item.result,
1382
+ }));
1383
+ }
1384
+ if (options.dryRun) {
1385
+ console.log(t('flow.health.fixDryRunNote', lang));
1386
+ }
1387
+ else {
1388
+ const applied = reconciled.filter((i) => i.result === 'applied').length;
1389
+ console.log(t('flow.health.fixAppliedTmpl', lang, { n: String(applied) }));
1390
+ }
1391
+ const skipped = reconciled.filter((i) => i.result === 'skipped-not-found').length;
1392
+ if (skipped > 0) {
1393
+ console.log(t('flow.health.fixSkippedTmpl', lang, { n: String(skipped) }));
1394
+ }
1395
+ console.log(t('flow.health.fixOnlyStatusNote', lang));
1396
+ console.log('');
1397
+ }
836
1398
  console.log(t('flow.health.title', lang) + '\n');
837
1399
  const result = mgr.healthCheck(options.quick ?? false);
838
1400
  // 图标映射
@@ -913,6 +1475,12 @@ export function registerFlowCommand(program) {
913
1475
  .description('交互式流水线向导,逐步推进阶段')
914
1476
  .action(async () => {
915
1477
  const lang = getCliLang(process.cwd());
1478
+ // 非 TTY 守卫:脚本/CI 中不渲染 ANSI 交互,输出等价指引并非 0 退出(T39)
1479
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1480
+ console.error(t('flow.wizard.nonTtyHint', lang));
1481
+ process.exitCode = 1;
1482
+ return;
1483
+ }
916
1484
  const mgr = createManager();
917
1485
  if (!mgr.isLoaded()) {
918
1486
  console.log(t('common.noInit', lang));
@@ -1056,16 +1624,14 @@ export function registerFlowCommand(program) {
1056
1624
  }
1057
1625
  }
1058
1626
  catch (err) {
1059
- // 并发冲突:本次未写入 flow.json,输出可重试提示并退出码 2(R3.2)
1627
+ // 并发写冲突:统一单点(i18n 文案 + 退出码 2)
1060
1628
  if (isFlowConcurrentError(err)) {
1061
- console.error(`[并发冲突] flow.json 已被其它进程修改(期望 revision=${err.expectedRevision},磁盘=${err.actualRevision})。`);
1062
- console.error('本次修改未写入。请重新执行该命令重试。');
1063
- process.exit(2);
1064
- }
1065
- // 非 TTY 环境或用户中断等异常
1066
- if (err instanceof Error) {
1067
- console.error(t('common.errorTmpl', lang, { msg: err.message }));
1629
+ handleConcurrentConflict(err, lang);
1068
1630
  }
1631
+ // 失败:统一错误模板 + 非 0 退出(T39)
1632
+ const msg = err instanceof Error ? err.message : String(err);
1633
+ console.error(t('common.errorTmpl', lang, { msg }));
1634
+ process.exitCode = 1;
1069
1635
  }
1070
1636
  });
1071
1637
  }
@@ -1073,6 +1639,33 @@ export function registerFlowCommand(program) {
1073
1639
  function createManager() {
1074
1640
  return new FlowManager(process.cwd());
1075
1641
  }
1642
+ /**
1643
+ * 断言不存在阻塞中的 open REV(REV 闭环兜底,B5-3 / REV-52-001)。
1644
+ * 命中 → 打印列出并返回命中的 REV 列表(调用方决定退出码与文案);无命中 → 空数组。
1645
+ * 单步 advance 与多步循环共用(禁止双实现)。
1646
+ * @param mgr FlowManager 实例
1647
+ * @param stage 阶段 ID(可选;未提供时返回空)
1648
+ * @param lang 语言标识(保留参数,供后续 i18n 化)
1649
+ */
1650
+ function assertNoBlockingOpenRev(mgr, stage, lang) {
1651
+ if (!stage) {
1652
+ return [];
1653
+ }
1654
+ // REV-007:补 `.` 分隔符(与 core flow-manager.ts 的 stageId + '.' 范式一致),避免 stageId 前缀重叠误拦
1655
+ const stageReviews = mgr.getReviewItems().filter((r) => r.op === stage || r.op.startsWith(stage + '.'));
1656
+ const blockingOpen = stageReviews.filter((r) => r.blocking !== false && r.status === 'open');
1657
+ if (blockingOpen.length > 0) {
1658
+ console.warn(t('flow.advance.revBlockedTitleTmpl', lang, { n: String(blockingOpen.length) }));
1659
+ for (const rev of blockingOpen) {
1660
+ console.warn(t('flow.advance.revBlockedItemTmpl', lang, {
1661
+ id: rev.id,
1662
+ title: rev.title,
1663
+ priority: rev.priority,
1664
+ }));
1665
+ }
1666
+ }
1667
+ return blockingOpen;
1668
+ }
1076
1669
  /** 格式化毫秒时长为人类可读形式 */
1077
1670
  function formatDuration(ms) {
1078
1671
  if (ms <= 0) {
@@ -1091,4 +1684,100 @@ function formatDuration(ms) {
1091
1684
  }
1092
1685
  return `${minutes}m ${seconds}s`;
1093
1686
  }
1687
+ /**
1688
+ * 归一化 stage.ops 为 [opId, state] 映射(类型守卫:跳过 null/undefined/数组)。
1689
+ * 供 flow status/overview 的 --json 结构化输出共用。
1690
+ */
1691
+ function collectOpStates(stage) {
1692
+ const opsMap = stage.ops && typeof stage.ops === 'object' && !Array.isArray(stage.ops)
1693
+ ? stage.ops
1694
+ : {};
1695
+ const result = {};
1696
+ for (const [opId, op] of Object.entries(opsMap)) {
1697
+ result[opId] = op?.state ?? '';
1698
+ }
1699
+ return result;
1700
+ }
1701
+ /**
1702
+ * 构建 `flow status --json` 的结构化输出(B1-1)。
1703
+ * 顶层对象 + schemaVersion:1;stages 为数组,ops 为 opId→state 映射;counts 为阶段计数。
1704
+ */
1705
+ function buildStatusJson(mgr) {
1706
+ const data = mgr.getData();
1707
+ if (!data) {
1708
+ return {
1709
+ schemaVersion: 1,
1710
+ pipeline: { phase: '', current: { stage: '', op: '' } },
1711
+ stages: [],
1712
+ counts: { total: 0, done: 0, active: 0 },
1713
+ };
1714
+ }
1715
+ const stages = Object.entries(data.stages).map(([id, stage]) => ({
1716
+ id,
1717
+ phase: stage.phase,
1718
+ status: stage.status,
1719
+ deps: stage.deps ?? [],
1720
+ ops: collectOpStates(stage),
1721
+ }));
1722
+ const total = stages.length;
1723
+ const done = stages.filter((s) => s.phase === 'done').length;
1724
+ return {
1725
+ schemaVersion: 1,
1726
+ pipeline: {
1727
+ phase: data.pipeline.phase,
1728
+ current: { stage: data.pipeline.current.stage, op: data.pipeline.current.op },
1729
+ },
1730
+ stages,
1731
+ counts: { total, done, active: total - done },
1732
+ };
1733
+ }
1734
+ /**
1735
+ * 构建 `flow overview --json` 的结构化输出(B1-5)。
1736
+ * 顶层对象 + schemaVersion:1;含阶段计数、审查统计、健康统计、日志数等。
1737
+ */
1738
+ function buildOverviewJson(mgr) {
1739
+ const data = mgr.getData();
1740
+ const phase = mgr.getPhase();
1741
+ const summary = mgr.getSummary();
1742
+ const health = mgr.healthCheck(true); // quick mode(与人类可读分支一致)
1743
+ const stages = data
1744
+ ? Object.entries(data.stages).map(([id, stage]) => {
1745
+ const ops = collectOpStates(stage);
1746
+ const states = Object.values(ops);
1747
+ return {
1748
+ id,
1749
+ phase: stage.phase,
1750
+ status: stage.status,
1751
+ opsTotal: states.length,
1752
+ opsDone: states.filter((s) => s === 'done').length,
1753
+ };
1754
+ })
1755
+ : [];
1756
+ const reviews = data?.reviews ?? [];
1757
+ const openReviews = reviews.filter((r) => r.status === 'open');
1758
+ return {
1759
+ schemaVersion: 1,
1760
+ phase: phase ?? '',
1761
+ current: {
1762
+ stage: data?.pipeline.current.stage ?? '',
1763
+ op: data?.pipeline.current.op ?? '',
1764
+ },
1765
+ summary,
1766
+ stages,
1767
+ reviews: {
1768
+ open: openReviews.length,
1769
+ blockingOpen: openReviews.filter((r) => r.blocking !== false).length,
1770
+ nonBlockingOpen: openReviews.filter((r) => r.blocking === false).length,
1771
+ resolved: reviews.filter((r) => r.status === 'resolved').length,
1772
+ closed: reviews.filter((r) => r.status === 'closed').length,
1773
+ },
1774
+ health: {
1775
+ ok: health.ok,
1776
+ pass: health.items.filter((i) => i.status === 'pass').length,
1777
+ warn: health.items.filter((i) => i.status === 'warn').length,
1778
+ fail: health.items.filter((i) => i.status === 'fail').length,
1779
+ },
1780
+ logs: data?.log.length ?? 0,
1781
+ };
1782
+ }
1094
1783
  //# sourceMappingURL=flow.js.map