openfeel 1.1.7 → 1.2.1

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 (115) hide show
  1. package/README.en.md +35 -8
  2. package/README.md +29 -4
  3. package/README.zh-CN.md +35 -8
  4. package/bin/openfeel.js +3 -2
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +7 -4
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/commands/config.d.ts.map +1 -1
  9. package/dist/commands/config.js +29 -4
  10. package/dist/commands/config.js.map +1 -1
  11. package/dist/commands/flow.d.ts.map +1 -1
  12. package/dist/commands/flow.js +385 -14
  13. package/dist/commands/flow.js.map +1 -1
  14. package/dist/commands/plan.d.ts.map +1 -1
  15. package/dist/commands/plan.js +19 -6
  16. package/dist/commands/plan.js.map +1 -1
  17. package/dist/commands/req.d.ts +15 -0
  18. package/dist/commands/req.d.ts.map +1 -0
  19. package/dist/commands/req.js +735 -0
  20. package/dist/commands/req.js.map +1 -0
  21. package/dist/commands/stage.d.ts.map +1 -1
  22. package/dist/commands/stage.js +7 -1
  23. package/dist/commands/stage.js.map +1 -1
  24. package/dist/core/config.d.ts +6 -4
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js +6 -5
  27. package/dist/core/config.js.map +1 -1
  28. package/dist/core/flow-archive/archive-file.d.ts +35 -0
  29. package/dist/core/flow-archive/archive-file.d.ts.map +1 -0
  30. package/dist/core/flow-archive/archive-file.js +3 -0
  31. package/dist/core/flow-archive/archive-file.js.map +1 -0
  32. package/dist/core/flow-archive/archive-health.d.ts +34 -0
  33. package/dist/core/flow-archive/archive-health.d.ts.map +1 -0
  34. package/dist/core/flow-archive/archive-health.js +76 -0
  35. package/dist/core/flow-archive/archive-health.js.map +1 -0
  36. package/dist/core/flow-archive/archive-ops.d.ts +68 -0
  37. package/dist/core/flow-archive/archive-ops.d.ts.map +1 -0
  38. package/dist/core/flow-archive/archive-ops.js +184 -0
  39. package/dist/core/flow-archive/archive-ops.js.map +1 -0
  40. package/dist/core/flow-archive/grouping.d.ts +58 -0
  41. package/dist/core/flow-archive/grouping.d.ts.map +1 -0
  42. package/dist/core/flow-archive/grouping.js +153 -0
  43. package/dist/core/flow-archive/grouping.js.map +1 -0
  44. package/dist/core/flow-manager.d.ts +67 -9
  45. package/dist/core/flow-manager.d.ts.map +1 -1
  46. package/dist/core/flow-manager.js +243 -51
  47. package/dist/core/flow-manager.js.map +1 -1
  48. package/dist/core/i18n-data/en.d.ts +1 -0
  49. package/dist/core/i18n-data/en.d.ts.map +1 -1
  50. package/dist/core/i18n-data/en.js +174 -4
  51. package/dist/core/i18n-data/en.js.map +1 -1
  52. package/dist/core/i18n-data/zh-CN.d.ts +1 -0
  53. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  54. package/dist/core/i18n-data/zh-CN.js +175 -4
  55. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  56. package/dist/core/i18n.js +4 -4
  57. package/dist/core/i18n.js.map +1 -1
  58. package/dist/core/managed-region.d.ts +16 -2
  59. package/dist/core/managed-region.d.ts.map +1 -1
  60. package/dist/core/managed-region.js +24 -3
  61. package/dist/core/managed-region.js.map +1 -1
  62. package/dist/core/metrics.d.ts +2 -0
  63. package/dist/core/metrics.d.ts.map +1 -1
  64. package/dist/core/metrics.js +15 -0
  65. package/dist/core/metrics.js.map +1 -1
  66. package/dist/core/model-config.d.ts +1 -1
  67. package/dist/core/model-config.d.ts.map +1 -1
  68. package/dist/core/model-config.js +2 -2
  69. package/dist/core/model-config.js.map +1 -1
  70. package/dist/core/opencode-config.js +1 -1
  71. package/dist/core/opencode-config.js.map +1 -1
  72. package/dist/core/pipeline-schema.d.ts +15 -0
  73. package/dist/core/pipeline-schema.d.ts.map +1 -1
  74. package/dist/core/pipeline-schema.js +17 -0
  75. package/dist/core/pipeline-schema.js.map +1 -1
  76. package/dist/core/plan/path.d.ts +17 -0
  77. package/dist/core/plan/path.d.ts.map +1 -1
  78. package/dist/core/plan/path.js +31 -0
  79. package/dist/core/plan/path.js.map +1 -1
  80. package/dist/core/plan/stage.d.ts +2 -1
  81. package/dist/core/plan/stage.d.ts.map +1 -1
  82. package/dist/core/plan/stage.js +7 -2
  83. package/dist/core/plan/stage.js.map +1 -1
  84. package/dist/core/req/aggregate.d.ts +62 -0
  85. package/dist/core/req/aggregate.d.ts.map +1 -0
  86. package/dist/core/req/aggregate.js +134 -0
  87. package/dist/core/req/aggregate.js.map +1 -0
  88. package/dist/core/req/body-file.d.ts +16 -0
  89. package/dist/core/req/body-file.d.ts.map +1 -0
  90. package/dist/core/req/body-file.js +84 -0
  91. package/dist/core/req/body-file.js.map +1 -0
  92. package/dist/core/req/requirement-ops.d.ts +116 -0
  93. package/dist/core/req/requirement-ops.d.ts.map +1 -0
  94. package/dist/core/req/requirement-ops.js +395 -0
  95. package/dist/core/req/requirement-ops.js.map +1 -0
  96. package/dist/core/req/types.d.ts +46 -0
  97. package/dist/core/req/types.d.ts.map +1 -0
  98. package/dist/core/req/types.js +7 -0
  99. package/dist/core/req/types.js.map +1 -0
  100. package/dist/core/runtime-log.d.ts +18 -0
  101. package/dist/core/runtime-log.d.ts.map +1 -1
  102. package/dist/core/runtime-log.js +72 -0
  103. package/dist/core/runtime-log.js.map +1 -1
  104. package/dist/core/session.d.ts +24 -0
  105. package/dist/core/session.d.ts.map +1 -0
  106. package/dist/core/session.js +107 -0
  107. package/dist/core/session.js.map +1 -0
  108. package/dist/core/setup.js +4 -4
  109. package/dist/core/template-loader.d.ts.map +1 -1
  110. package/dist/core/template-loader.js +3160 -6571
  111. package/dist/core/template-loader.js.map +1 -1
  112. package/dist/core/update.d.ts.map +1 -1
  113. package/dist/core/update.js +770 -26
  114. package/dist/core/update.js.map +1 -1
  115. package/package.json +1 -1
@@ -1,15 +1,21 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { existsSync, copyFileSync, readFileSync, rmSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { FlowManager, isFlowConcurrentError, normalizeAgentName, detectFillState, PHASE_PATH_MAX_DEPTH } from '../core/flow-manager.js';
5
- import { PipelinePhaseSchema, PIPELINE_PHASES } from '../core/pipeline-schema.js';
6
- import { validateStageId, suggestStageId, normalizeStageId, findStageStatusPath } from '../core/plan/path.js';
4
+ import { FlowManager, isFlowConcurrentError, normalizeAgentName, detectFillState, MODE_DERIVED_TIER, PHASE_PATH_MAX_DEPTH } from '../core/flow-manager.js';
5
+ import { PipelinePhaseSchema, PIPELINE_PHASES, REVIEW_TIERS, TIER_PRUNED_PHASES } from '../core/pipeline-schema.js';
6
+ import { validateStageId, suggestStageId, normalizeStageId, findStageStatusPath, isArchivedVersionOf } from '../core/plan/path.js';
7
7
  import { getConfigFieldLegalValues, setConfigValue } from '../core/config.js';
8
8
  import { parse as parseYaml } from 'yaml';
9
- import { MetricsStore } from '../core/metrics.js';
9
+ import { MetricsStore, PHASE_AGENT_MAP } from '../core/metrics.js';
10
+ import { runtimeLog } from '../core/runtime-log.js';
10
11
  import { addReviewEntry } from '../core/view/entry.js';
11
12
  import { handleAddStageError, handleConcurrentConflict } from './shared/errors.js';
12
13
  import { t, getCliLang } from '../core/i18n.js';
14
+ import { ARCHIVE_SCHEMA_VERSION } from '../core/flow-archive/archive-file.js';
15
+ import { listArchivableCandidates, listBlockingOpenRevsForVersion, splitForArchive } from '../core/flow-archive/grouping.js';
16
+ import { normalizeArchiveVersion, writeArchiveFile, addArchivedVersion, applyArchivePipelineDisposition, listArchiveVersions, readArchiveFile } from '../core/flow-archive/archive-ops.js';
17
+ import { archiveHealthItems } from '../core/flow-archive/archive-health.js';
18
+ import { requirementHealthItems } from '../core/req/aggregate.js'; // 需求对账项(stage-83 T3)
13
19
  export function registerFlowCommand(program) {
14
20
  const flow = program
15
21
  .command('flow')
@@ -343,7 +349,7 @@ export function registerFlowCommand(program) {
343
349
  .command('phases')
344
350
  .description('列出全部合法 phase 及其流转映射(自描述)')
345
351
  .option('--json', 'Output { phases, transitions, advanceAccepted, schemaVersion } as JSON')
346
- .option('--mode <mode>', '显式指定工作流模式(full | fast;缺省经 --stage / 当前阶段派生)')
352
+ .option('--mode <mode>', '显式指定工作流模式(full | fast | general;缺省经 --stage / 当前阶段派生)')
347
353
  .option('--stage <id>', '按指定阶段的模式展示转移表(缺省 = 当前阶段)')
348
354
  .action((options) => {
349
355
  const projectPath = process.cwd();
@@ -394,7 +400,8 @@ export function registerFlowCommand(program) {
394
400
  if (options.json) {
395
401
  // advanceAccepted = 内置 15 phase(flow advance 的推进白名单),与 phases(运行时存在视图,可含自定义)显式区分
396
402
  // 既有三字段(phases/transitions/advanceAccepted)+ transitionsDiff 逐字保留;schemaVersion 与 workflowMode 追加于末位(B1-6 / stage-75 op-002)
397
- console.log(JSON.stringify({ schemaVersion: 1, phases, transitions, advanceAccepted: [...PIPELINE_PHASES], transitionsDiff, workflowMode: derivedMode }, null, 2));
403
+ // reviewTier 再追加于末位(stage-92 op-001;既有 6 键逐字保留、顺序不变)
404
+ console.log(JSON.stringify({ schemaVersion: 1, phases, transitions, advanceAccepted: [...PIPELINE_PHASES], transitionsDiff, workflowMode: derivedMode, reviewTier: MODE_DERIVED_TIER[derivedMode] }, null, 2));
398
405
  return;
399
406
  }
400
407
  console.log(t('flow.phases.title', lang));
@@ -406,6 +413,9 @@ export function registerFlowCommand(program) {
406
413
  // 模式分节行(stage-75 op-002:模式 + 来源 + 查看指引)
407
414
  console.log(t('flow.mode.lineTmpl', lang, { mode: derivedMode, source: explicitMode ? '--mode' : resolved.source }));
408
415
  console.log(t('flow.phases.modeHint', lang));
416
+ // 审查档位分节行(stage-92 op-001:档位 + 来源(模式派生)+ 查看指引;档位仅由模式**派生展示**,不改变推进语义)
417
+ console.log(t('flow.phases.tierLineTmpl', lang, { tier: MODE_DERIVED_TIER[derivedMode], mode: derivedMode }));
418
+ console.log(t('flow.phases.tierHint', lang));
409
419
  console.log(t('flow.phases.transitionsLabel', lang) + ':');
410
420
  for (const [key, targets] of Object.entries(transitions)) {
411
421
  console.log(` ${key} → [${targets.join(', ')}]`);
@@ -470,6 +480,7 @@ export function registerFlowCommand(program) {
470
480
  const modes = {
471
481
  full: mgr.getPipelineTransitions('full'),
472
482
  fast: mgr.getPipelineTransitions('fast'),
483
+ general: mgr.getPipelineTransitions('general'),
473
484
  };
474
485
  if (options.json) {
475
486
  // 未初始化亦保纯 JSON 单文档(stage 空串 + 无 stage 域级联解析值)
@@ -625,7 +636,12 @@ export function registerFlowCommand(program) {
625
636
  .filter(Boolean);
626
637
  // 悬空依赖:命令层强制校验(核心层仅 warn),非法一律 exit 1 且不写盘
627
638
  const known = new Set(Object.keys(data.stages).map((k) => normalizeStageId(k) ?? k));
628
- const invalid = deps.filter((d) => !known.has(normalizeStageId(d) ?? d));
639
+ // 已归档依赖(显式版本前缀 ∧ 版本 ∈ archived_versions)→ 视为已满足(同 plan.ts 单一口径;恒传原始 d)
640
+ const archivedOk = deps.filter((d) => !known.has(normalizeStageId(d) ?? d) && isArchivedVersionOf(d, data.meta?.archived_versions));
641
+ const invalid = deps.filter((d) => !known.has(normalizeStageId(d) ?? d) && !isArchivedVersionOf(d, data.meta?.archived_versions));
642
+ if (archivedOk.length > 0) {
643
+ console.log(t('stage.set.archivedDepsTmpl', lang, { deps: archivedOk.join(', ') }));
644
+ }
629
645
  if (invalid.length > 0) {
630
646
  console.error(t('stage.set.depsDanglingTmpl', lang, { deps: invalid.join(', ') }));
631
647
  process.exit(1);
@@ -712,6 +728,7 @@ export function registerFlowCommand(program) {
712
728
  .option('--stage <id>', '阶段 ID(如 stage-03),必须指定')
713
729
  .option('--force', '强制执行(跳过非法 phase 校验和阶段跳跃检查,但不可绕过 REV 阻塞检查)')
714
730
  .option('--dry-run', '仅验证不执行修改(预览输出)。与 --force 组合时跳过校验但仍不执行修改')
731
+ .option('--review-tier <tier>', '审查档位(full / lean / skip;缺省=按模式派生;仅显式声明时裁剪审查关,不可绕过 REV 阻塞检查)')
715
732
  .option('--quiet', '静默非错误输出(成功确认与 Git 警告均不打印)')
716
733
  .action((options) => {
717
734
  const lang = getCliLang(process.cwd());
@@ -720,6 +737,19 @@ export function registerFlowCommand(program) {
720
737
  console.error(t('flow.advance.errorNoStage', lang));
721
738
  process.exit(1);
722
739
  }
740
+ // 审查档位值域校验(stage-92 op-001;fail-fast 且**先于任何取值 / 路径解析 / autoRepair 写盘**)
741
+ // 复用 stage.set.invalidValueTmpl(与 flow phases --mode 同款,零新增键);非法 → exit 1 零写盘
742
+ let explicitTier;
743
+ if (options.reviewTier !== undefined) {
744
+ if (!REVIEW_TIERS.includes(options.reviewTier)) {
745
+ console.error(t('stage.set.invalidValueTmpl', lang, { field: '审查档位', value: options.reviewTier, allowed: REVIEW_TIERS.join(', ') }));
746
+ process.exit(1);
747
+ return;
748
+ }
749
+ explicitTier = options.reviewTier;
750
+ }
751
+ // 生效档位:显式声明 ?? full(**不传 = 零裁剪**;派生档仅用于 `flow phases` 展示与提示,不改变推进语义)
752
+ const effTier = explicitTier ?? 'full';
723
753
  const mgr = createManager();
724
754
  if (!mgr.isLoaded()) {
725
755
  console.error(t('common.errorNoInit', lang));
@@ -771,11 +801,11 @@ export function registerFlowCommand(program) {
771
801
  // - 已在 to → no-op 成功
772
802
  let phasePath = [options.to];
773
803
  if (!options.force) {
774
- const pathResult = mgr.findPhasePath(stageArg, options.to);
804
+ const pathResult = mgr.findPhasePath(stageArg, options.to, effTier);
775
805
  if (pathResult.reason === 'no-path' || pathResult.reason === 'ambiguous' || pathResult.reason === 'depth-exceeded') {
776
806
  const data = mgr.getData();
777
807
  const currentPhase = data?.stages[stageArg]?.phase ?? t('common.unknown', lang);
778
- const availableTargets = mgr.getAvailablePhases(stageArg);
808
+ const availableTargets = mgr.getAvailablePhases(stageArg, effTier);
779
809
  if (pathResult.reason === 'ambiguous') {
780
810
  console.error(t('flow.advance.ambiguousTmpl', lang));
781
811
  }
@@ -783,6 +813,12 @@ export function registerFlowCommand(program) {
783
813
  console.error(t('flow.advance.depthExceededTmpl', lang, { max: String(PHASE_PATH_MAX_DEPTH) }));
784
814
  }
785
815
  else {
816
+ // general(无流水线)→ 显式错误引导(防御路径:常规写路径不产生 general 阶段态;REV-90-005 / D-90-3)
817
+ if (mgr.resolveStageWorkflowMode(stageArg).value === 'general') {
818
+ console.error(t('flow.advance.generalNoPipeline', lang, { stage: stageArg }));
819
+ process.exit(1);
820
+ return;
821
+ }
786
822
  console.error(t('flow.advance.noPathTmpl', lang, {
787
823
  from: currentPhase,
788
824
  to: options.to,
@@ -819,6 +855,14 @@ export function registerFlowCommand(program) {
819
855
  console.warn(t('flow.advance.warnSkipReview', lang));
820
856
  }
821
857
  }
858
+ // 审查档位协同警告(stage-92 op-001 / D-92-4):**仅显式声明**非全量档位时输出一行
859
+ // 非阻断(退出码不变);`--review-tier full` 与缺省不告警 ⟹ 既有流程零输出回归
860
+ if (explicitTier === 'lean' || explicitTier === 'skip') {
861
+ console.warn(t('flow.advance.warnTierTmpl', lang, {
862
+ tier: explicitTier,
863
+ gates: TIER_PRUNED_PHASES[explicitTier].join(', '),
864
+ }));
865
+ }
822
866
  // REV 闭环(命令层兜底):推进到 done 时检查 blocking REV(单步入口;多步循环每步后另复检,B5-3)
823
867
  if (options.to === 'done' && options.stage) {
824
868
  const blockingOpen = assertNoBlockingOpenRev(mgr, stageArg, lang);
@@ -843,6 +887,13 @@ export function registerFlowCommand(program) {
843
887
  console.log(` ` + t('common.stage', lang) + `: ${stageArg}`);
844
888
  console.log(` ` + t('flow.advance.dryRunFrom', lang) + `: ${startPhase}`);
845
889
  console.log(` ` + t('flow.advance.dryRunTo', lang) + `: ${options.to}`);
890
+ // 审查档位行(stage-92 op-001):仅显式声明时追加(缺省零输出回归);档位为空裁剪时 gates 显示「(无)」
891
+ if (explicitTier !== undefined) {
892
+ console.log(` ` + t('flow.advance.dryRunTierTmpl', lang, {
893
+ tier: explicitTier,
894
+ gates: TIER_PRUNED_PHASES[explicitTier].join(', ') || t('common.none', lang),
895
+ }));
896
+ }
846
897
  if (phasePath.length > 1) {
847
898
  const fullPath = [startPhase, ...phasePath].join(t('flow.advance.pathArrow', lang));
848
899
  console.log(` ` + t('flow.advance.pathTitle', lang) + ` ${fullPath}`);
@@ -853,13 +904,19 @@ export function registerFlowCommand(program) {
853
904
  }
854
905
  // 逐步推进(B5-2 / A3):唯一路径 → 依次调用 advanceStagePhase(保留每步校验/日志/checkpoint)
855
906
  // 中间态不回滚(A3);失败 → 输出「已完成 / 失败点 / 剩余路径」+ exit 1
907
+ // T1:recordRun 接线(命令层;--dry-run 已于上方提前 return——天然排除)
908
+ const metricsStore = MetricsStore.getInstance();
909
+ metricsStore.load(); // 读盘合并(防 save 覆盖历史记录);幂等
910
+ const logLenBefore = mgr.getData()?.log.length ?? 0; // 基线回扫上界(排除本次写入;REV-86-002)
911
+ let metricsRecorded = false;
912
+ let prevStepDoneMs = 0; // 多步时上一步完成时刻(i>0 的窗口基线)
856
913
  const totalSteps = phasePath.length;
857
914
  let archivedAny = false;
858
915
  for (let i = 0; i < totalSteps; i++) {
859
916
  const next = phasePath[i];
860
917
  let archived = false;
861
918
  try {
862
- archived = mgr.advanceStagePhase(stageArg, next, 'cli');
919
+ archived = mgr.advanceStagePhase(stageArg, next, 'cli', explicitTier ? { reviewTier: explicitTier } : undefined);
863
920
  }
864
921
  catch (err) {
865
922
  if (totalSteps > 1) {
@@ -874,6 +931,21 @@ export function registerFlowCommand(program) {
874
931
  }
875
932
  archivedAny = archivedAny || archived;
876
933
  mgr.save();
934
+ // T1:对刚退出的 fromPhase 记一条 run(窗口 = 进入该 phase 的转换 → 本次退出)
935
+ const exitedPhase = i > 0 ? phasePath[i - 1] : startPhase;
936
+ const agentName = PHASE_AGENT_MAP[exitedPhase];
937
+ if (agentName) {
938
+ const baseMs = i > 0 ? prevStepDoneMs : findPhaseEnteredAtMs(mgr, stageArg, exitedPhase, logLenBefore);
939
+ if (baseMs !== null && baseMs > 0) {
940
+ metricsStore.recordRun(agentName, Date.now() - baseMs, 'success', 0);
941
+ metricsRecorded = true;
942
+ }
943
+ else {
944
+ // 无基线 → 跳过不记(不写 0 值污染 avg);debug 留痕(默认关,不扰输出)
945
+ runtimeLog('debug', 'metrics skip (no baseline): stage=' + stageArg + ' phase=' + exitedPhase);
946
+ }
947
+ }
948
+ prevStepDoneMs = Date.now();
877
949
  // 归档 commit 必须在 flow.json save 之后执行,确保 commit 包含本次 phase 变更
878
950
  if (archived) {
879
951
  mgr.autoCommitOnDone(stageArg);
@@ -902,6 +974,10 @@ export function registerFlowCommand(program) {
902
974
  }
903
975
  }
904
976
  }
977
+ // T1:仅本命令内有记录时落盘一次(低并发非关键路径;推进失败提前 exit 则不 save)
978
+ if (metricsRecorded) {
979
+ metricsStore.save();
980
+ }
905
981
  // 成功确认(单步与多步统一);--quiet 完全静默
906
982
  if (!options.quiet) {
907
983
  console.log(t('flow.advance.okTmpl', lang, { stage: stageArg, to: options.to }));
@@ -1513,6 +1589,253 @@ export function registerFlowCommand(program) {
1513
1589
  process.exit(1);
1514
1590
  }
1515
1591
  });
1592
+ // flow archive <version> — flow.json 版本分片归档(裁定 O2;与顶层 openfeel archive〔阶段摘要〕职责分离,裁定 O3)
1593
+ const archiveCmd = flow
1594
+ .command('archive')
1595
+ .description('flow.json 版本分片归档(按版本号归档已收官历史)')
1596
+ .argument('<version>', '版本号(如 v1.1.7;前缀 v 可省略)')
1597
+ .option('--dry-run', '仅预览(判据门照常校验;零写盘)')
1598
+ .option('--json', '以 JSON 输出 (machine-readable, single JSON document)')
1599
+ .action((versionInput, options) => {
1600
+ const projectPath = process.cwd();
1601
+ const lang = getCliLang(projectPath);
1602
+ const mgr = createManager();
1603
+ // --json 失败出口统一:纯 JSON 单文档 + exit 1(不混人类文案、不走 stderr)
1604
+ const failJson = (payload) => {
1605
+ console.log(JSON.stringify({ schemaVersion: 1, ...payload }, null, 2));
1606
+ process.exitCode = 1;
1607
+ };
1608
+ if (!mgr.isLoaded()) {
1609
+ if (options.json) {
1610
+ failJson({ error: 'no-init' });
1611
+ return;
1612
+ }
1613
+ console.error(t('common.errorNoInit', lang));
1614
+ process.exitCode = 1;
1615
+ return;
1616
+ }
1617
+ // 版本归一(REV-79-004):去 v 再加 v;非法 → exit 1 零写盘
1618
+ const version = normalizeArchiveVersion(versionInput);
1619
+ if (!version) {
1620
+ if (options.json) {
1621
+ failJson({ error: 'invalid-version', input: versionInput });
1622
+ return;
1623
+ }
1624
+ console.error(t('flow.archive.invalidVersionTmpl', lang, { input: versionInput }));
1625
+ process.exitCode = 1;
1626
+ return;
1627
+ }
1628
+ const data = mgr.getData();
1629
+ // 版本存在性门(前置;REV-79-001):无候选(不存在/已归档)→ 拒绝零写盘;覆盖写仅限活跃集仍含该版本条目
1630
+ const candidate = listArchivableCandidates(data).find((c) => c.version === version);
1631
+ if (!candidate) {
1632
+ if (options.json) {
1633
+ failJson({ error: 'version-not-active', version });
1634
+ return;
1635
+ }
1636
+ console.error(t('flow.archive.versionNotActiveTmpl', lang, { version }));
1637
+ process.exitCode = 1;
1638
+ return;
1639
+ }
1640
+ // 判据门:全 done(candidate.allDone 空集守卫)+ 无 blocking open REV(全仓唯一谓词,REV-79-002)
1641
+ const notDone = candidate.stages.filter((id) => data.stages[id]?.phase !== 'done');
1642
+ const blockingOpen = listBlockingOpenRevsForVersion(data, version);
1643
+ if (!candidate.allDone || blockingOpen.length > 0) {
1644
+ if (options.json) {
1645
+ failJson({ error: 'gate-rejected', version, notDone, blockingOpen: blockingOpen.map((r) => r.id) });
1646
+ return;
1647
+ }
1648
+ if (notDone.length > 0) {
1649
+ console.error(t('flow.archive.gateNotDoneTitleTmpl', lang, { version, n: String(notDone.length) }));
1650
+ for (const id of notDone) {
1651
+ console.error(t('flow.archive.gateItemTmpl', lang, { item: id + ' [' + (data.stages[id]?.phase ?? '') + ']' }));
1652
+ }
1653
+ }
1654
+ if (blockingOpen.length > 0) {
1655
+ console.error(t('flow.archive.gateBlockingRevTitleTmpl', lang, { version, n: String(blockingOpen.length) }));
1656
+ for (const r of blockingOpen) {
1657
+ console.error(t('flow.archive.gateItemTmpl', lang, { item: r.id + ': ' + r.title + ' (priority=' + r.priority + ')' }));
1658
+ }
1659
+ }
1660
+ console.error(t('flow.archive.gateRejected', lang));
1661
+ process.exitCode = 1;
1662
+ return;
1663
+ }
1664
+ // 切分(纯函数;禁命令层第二套解析/切分)
1665
+ const { archive, remainder } = splitForArchive(data, version);
1666
+ const counts = archive.counts;
1667
+ // 装配(REV-79-009):SplitResult.archive 不含 schemaVersion / archived_at
1668
+ const file = { ...archive, schemaVersion: ARCHIVE_SCHEMA_VERSION, archived_at: new Date().toISOString() };
1669
+ // --dry-run:纯内存推演(不写归档文件 / 不 addArchivedVersion / 不 appendLog / 不 save)——四写盘点零触碰
1670
+ if (options.dryRun) {
1671
+ // 深拷 pipeline(REV-79-015):处置 helper 会就地改 current/phase,预览不得污染 live data
1672
+ const previewData = {
1673
+ ...data,
1674
+ stages: remainder.stages,
1675
+ reviews: remainder.reviews,
1676
+ log: remainder.log,
1677
+ pipeline: { ...data.pipeline, current: { ...data.pipeline.current } },
1678
+ };
1679
+ const preview = applyArchivePipelineDisposition(previewData, version);
1680
+ if (options.json) {
1681
+ console.log(JSON.stringify({ schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log, dryRun: true }, null, 2));
1682
+ return;
1683
+ }
1684
+ console.log(t('flow.archive.previewTmpl', lang, { version, stages: String(counts.stages), reviews: String(counts.reviews), log: String(counts.log) }));
1685
+ console.log(t('flow.archive.pipelineTmpl', lang, {
1686
+ current: preview.currentCleared ? t('common.none', lang) : (previewData.pipeline.current.stage || t('common.none', lang)),
1687
+ phase: preview.phase,
1688
+ }));
1689
+ console.log(t('flow.archive.previewNoWrite', lang));
1690
+ return;
1691
+ }
1692
+ // ① 先写归档文件(新增;独立锁 flow-archive,与 flow 锁不嵌套)
1693
+ try {
1694
+ writeArchiveFile(projectPath, file);
1695
+ }
1696
+ catch (err) {
1697
+ // 写盘失败(REV-79-013):内存未改动(移除/索引/审计均在其后),无需回滚;exit 1 零 flow.json 变更
1698
+ if (options.json) {
1699
+ failJson({ error: 'archive-write-failed', version });
1700
+ return;
1701
+ }
1702
+ console.error(t('common.errorTmpl', lang, { msg: err instanceof Error ? err.message : String(err) }));
1703
+ process.exitCode = 1;
1704
+ return;
1705
+ }
1706
+ // ② 内存移除(stages / reviews / log 三段)
1707
+ data.stages = remainder.stages;
1708
+ data.reviews = remainder.reviews;
1709
+ data.log = remainder.log;
1710
+ // ③ 索引(幂等去重;纯内存)
1711
+ const added = addArchivedVersion(data, version);
1712
+ // ④ pipeline 处置(纯内存;空集 → active,与 flow-manager.ts:1495-1496 守卫同效)
1713
+ const disposed = applyArchivePipelineDisposition(data, version);
1714
+ // ⑤ 审计(须晚于 ②,记录在归档后 flow.json——不混入被归档段)
1715
+ mgr.appendLog({ time: '', agent: 'cli', action: 'archive_flow_version', detail: { version, stages: counts.stages, reviews: counts.reviews, log: counts.log } });
1716
+ // ⑥ 后 save()(顺序不变量:归档文件写成功之前不改 flow.json)
1717
+ try {
1718
+ mgr.save();
1719
+ }
1720
+ catch (err) {
1721
+ if (isFlowConcurrentError(err)) {
1722
+ handleConcurrentConflict(err, lang); // exit 2(归档文件已落盘;重跑幂等收敛)
1723
+ return;
1724
+ }
1725
+ console.error(t('common.errorTmpl', lang, { msg: err instanceof Error ? err.message : String(err) }));
1726
+ process.exitCode = 1;
1727
+ return;
1728
+ }
1729
+ if (options.json) {
1730
+ console.log(JSON.stringify({ schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log, dryRun: false }, null, 2));
1731
+ return;
1732
+ }
1733
+ if (added) {
1734
+ console.log(t('flow.archive.doneTmpl', lang, { version, stages: String(counts.stages), reviews: String(counts.reviews), log: String(counts.log) }));
1735
+ }
1736
+ else {
1737
+ console.log(t('flow.archive.rearchivedTmpl', lang, { version }));
1738
+ }
1739
+ console.log(t('flow.archive.pipelineTmpl', lang, {
1740
+ current: disposed.currentCleared ? t('common.none', lang) : (data.pipeline.current.stage || t('common.none', lang)),
1741
+ phase: disposed.phase,
1742
+ }));
1743
+ });
1744
+ // flow archive list — 列出已归档版本及计数(只读;引擎字典序)
1745
+ archiveCmd
1746
+ .command('list')
1747
+ .description('列出已归档版本及计数(只读)')
1748
+ .option('--json', '以 JSON 输出 (machine-readable, single JSON document)')
1749
+ .action((options, command) => {
1750
+ const projectPath = process.cwd();
1751
+ const lang = getCliLang(projectPath);
1752
+ // 父命令 archive 亦声明 --json,Commander 派发子命令前会先由父命令消费该标志
1753
+ // → 合并父级 opts(否则 `flow archive list --json` 的 --json 不可达;REV-79-018)
1754
+ const json = options.json === true || command.parent?.opts().json === true;
1755
+ // 引擎字典序(零命令层第二套排序);逐文件 counts 经读侧复用(损坏 → null)
1756
+ const rows = listArchiveVersions(projectPath).map((v) => ({
1757
+ version: v,
1758
+ counts: readArchiveFile(projectPath, v)?.counts ?? null,
1759
+ }));
1760
+ if (json) {
1761
+ console.log(JSON.stringify({ schemaVersion: 1, versions: rows }, null, 2));
1762
+ return;
1763
+ }
1764
+ if (rows.length === 0) {
1765
+ console.log(t('flow.archive.list.empty', lang));
1766
+ return;
1767
+ }
1768
+ console.log(t('flow.archive.list.title', lang));
1769
+ for (const r of rows) {
1770
+ if (r.counts) {
1771
+ console.log(t('flow.archive.list.itemTmpl', lang, {
1772
+ version: r.version, stages: String(r.counts.stages), reviews: String(r.counts.reviews), log: String(r.counts.log),
1773
+ }));
1774
+ }
1775
+ else {
1776
+ console.log(t('flow.archive.list.unreadableTmpl', lang, { version: r.version }));
1777
+ }
1778
+ }
1779
+ // 只读保证:不 save()、不 appendLog、不读 flow.json
1780
+ });
1781
+ // flow archive show <version> — 查看指定归档版本(只读)
1782
+ archiveCmd
1783
+ .command('show')
1784
+ .description('查看指定归档版本(只读)')
1785
+ .argument('<version>', '版本号(如 v1.1.7;前缀 v 可省略)')
1786
+ .option('--json', '以 JSON 输出(全量,含 log / reviews)')
1787
+ .action((versionInput, options, command) => {
1788
+ const projectPath = process.cwd();
1789
+ const lang = getCliLang(projectPath);
1790
+ // 同 list:合并父级 --json(REV-79-018)
1791
+ const json = options.json === true || command.parent?.opts().json === true;
1792
+ const available = listArchiveVersions(projectPath);
1793
+ // 版本归一(前缀兼容):非法 → exit 1 零写盘
1794
+ const version = normalizeArchiveVersion(versionInput);
1795
+ if (!version) {
1796
+ if (json) {
1797
+ console.log(JSON.stringify({ schemaVersion: 1, error: 'invalid-version', input: versionInput, available }, null, 2));
1798
+ process.exitCode = 1;
1799
+ return;
1800
+ }
1801
+ console.error(t('flow.archive.invalidVersionTmpl', lang, { input: versionInput }));
1802
+ process.exitCode = 1;
1803
+ return;
1804
+ }
1805
+ const file = readArchiveFile(projectPath, version);
1806
+ if (!file) {
1807
+ if (json) {
1808
+ console.log(JSON.stringify({ schemaVersion: 1, error: 'not-found', version, available }, null, 2));
1809
+ process.exitCode = 1;
1810
+ return;
1811
+ }
1812
+ console.error(t('flow.archive.show.notFoundTmpl', lang, {
1813
+ version, versions: available.length > 0 ? available.join(', ') : t('common.none', lang),
1814
+ }));
1815
+ process.exitCode = 1;
1816
+ return;
1817
+ }
1818
+ // --json = 全量载荷(ArchiveFile 原文自带 schemaVersion:1,含 log / reviews)——显式请求才取全量(REV-79-003)
1819
+ if (json) {
1820
+ console.log(JSON.stringify(file, null, 2));
1821
+ return;
1822
+ }
1823
+ console.log(t('flow.archive.show.titleTmpl', lang, { version, at: typeof file.archived_at === 'string' ? file.archived_at : '' }));
1824
+ console.log(t('flow.archive.show.countsTmpl', lang, {
1825
+ stages: String(file.counts.stages), reviews: String(file.counts.reviews), log: String(file.counts.log),
1826
+ }));
1827
+ console.log(t('flow.archive.show.stagesLabel', lang));
1828
+ const entries = Object.entries(file.stages ?? {});
1829
+ if (entries.length === 0) {
1830
+ console.log(t('common.noData', lang));
1831
+ }
1832
+ else {
1833
+ for (const [id, stage] of entries) {
1834
+ console.log(t('flow.archive.show.stageLineTmpl', lang, { stage: id, phase: stage?.phase ?? '' }));
1835
+ }
1836
+ }
1837
+ console.log(t('flow.archive.show.fullHint', lang));
1838
+ });
1516
1839
  // flow health [--quick]
1517
1840
  flow
1518
1841
  .command('health')
@@ -1524,6 +1847,15 @@ export function registerFlowCommand(program) {
1524
1847
  .action((options) => {
1525
1848
  const lang = getCliLang(process.cwd());
1526
1849
  const mgr = createManager();
1850
+ const quick = options.quick ?? false;
1851
+ // 归档一致性检查项(T2):warn-only;--quick 跳过;flow.json 未加载 → 跳过(与既有检查降级一致)
1852
+ const archiveItems = !quick && mgr.isLoaded()
1853
+ ? archiveHealthItems({ projectPath: process.cwd(), data: mgr.getData() })
1854
+ : [];
1855
+ // 需求对账检查项(stage-83 T3):warn-only(悬空关联 / done 漂移);--quick 跳过;未加载 → 跳过
1856
+ const reqItems = !quick && mgr.isLoaded()
1857
+ ? requirementHealthItems({ data: mgr.getData(), lang })
1858
+ : [];
1527
1859
  // B2-2:--fix 状态对账(仅回写 status.md「状态」字段;--dry-run 预览不写盘)
1528
1860
  let reconciled;
1529
1861
  if (options.fix) {
@@ -1540,8 +1872,14 @@ export function registerFlowCommand(program) {
1540
1872
  }
1541
1873
  // B1-3:--json 分支纯 JSON 单文档;退出码语义不变(有 fail → 非 0)
1542
1874
  if (options.json) {
1543
- const report = mgr.getHealthReport(options.quick ?? false);
1544
- const payload = { schemaVersion: 1, ok: report.ok, items: report.items };
1875
+ const report = mgr.getHealthReport(quick);
1876
+ // 合并追加归档一致性项(warn-only,恒无 fail → ok / 退出码不变)
1877
+ const items = [
1878
+ ...report.items,
1879
+ ...archiveItems.map((i) => ({ name: i.section, status: i.status, detail: i.message })),
1880
+ ...reqItems.map((i) => ({ name: i.section, status: i.status, detail: i.message })),
1881
+ ];
1882
+ const payload = { schemaVersion: 1, ok: report.ok, items };
1545
1883
  // 与 op-001 的 health --json 合并字段(--fix 时追加 reconciled)
1546
1884
  if (reconciled) {
1547
1885
  payload.reconciled = reconciled;
@@ -1578,7 +1916,7 @@ export function registerFlowCommand(program) {
1578
1916
  console.log('');
1579
1917
  }
1580
1918
  console.log(t('flow.health.title', lang) + '\n');
1581
- const result = mgr.healthCheck(options.quick ?? false);
1919
+ const result = mgr.healthCheck(quick);
1582
1920
  // 图标映射
1583
1921
  const icon = (status) => {
1584
1922
  if (status === 'pass')
@@ -1587,7 +1925,8 @@ export function registerFlowCommand(program) {
1587
1925
  return '⚠️ ';
1588
1926
  return '❌';
1589
1927
  };
1590
- for (const item of result.items) {
1928
+ // 合并追加归档一致性项(warn-only;--quick 时 archiveItems 恒空)
1929
+ for (const item of [...result.items, ...archiveItems, ...reqItems]) {
1591
1930
  console.log(`${icon(item.status)} ${item.section}: ${item.message}`);
1592
1931
  }
1593
1932
  console.log('');
@@ -1879,6 +2218,38 @@ function hasExplicitProjectDefault(projectPath, key) {
1879
2218
  return false;
1880
2219
  }
1881
2220
  }
2221
+ /**
2222
+ * 求「进入 fromPhase 的那次转换」的时间戳(T1 duration 基线;REV-86-002 口径)。
2223
+ * 回扫范围 [0, logLenBefore)——本次命令写入之前的存量条目(天然排除本次新写入);
2224
+ * 按 detail.to === fromPhase 匹配 advance/reset 转换条目;plan_pending 无转换条目时兜底 register_stage;
2225
+ * 无基线返回 null(调用方跳过不记——不写 0 值)。
2226
+ */
2227
+ function findPhaseEnteredAtMs(mgr, stageArg, fromPhase, logLenBefore) {
2228
+ const log = mgr.getData()?.log ?? [];
2229
+ const upper = Math.min(logLenBefore, log.length);
2230
+ for (let k = upper - 1; k >= 0; k--) {
2231
+ const e = log[k];
2232
+ if (e.action !== 'advance_stage_phase' && e.action !== 'reset_stage_phase') {
2233
+ continue;
2234
+ }
2235
+ if (e.detail.stageName !== stageArg || e.detail.to !== fromPhase) {
2236
+ continue;
2237
+ }
2238
+ const ms = Date.parse(e.time);
2239
+ return Number.isNaN(ms) ? null : ms;
2240
+ }
2241
+ if (fromPhase === 'plan_pending') {
2242
+ for (let k = upper - 1; k >= 0; k--) {
2243
+ const e = log[k];
2244
+ if (e.action !== 'register_stage' || e.detail.stageName !== stageArg) {
2245
+ continue;
2246
+ }
2247
+ const ms = Date.parse(e.time);
2248
+ return Number.isNaN(ms) ? null : ms;
2249
+ }
2250
+ }
2251
+ return null;
2252
+ }
1882
2253
  /** 创建 FlowManager 实例(使用当前工作目录) */
1883
2254
  function createManager() {
1884
2255
  return new FlowManager(process.cwd());