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.
- package/README.en.md +35 -8
- package/README.md +29 -4
- package/README.zh-CN.md +35 -8
- package/bin/openfeel.js +3 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +7 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +29 -4
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/flow.d.ts.map +1 -1
- package/dist/commands/flow.js +385 -14
- package/dist/commands/flow.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +19 -6
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/req.d.ts +15 -0
- package/dist/commands/req.d.ts.map +1 -0
- package/dist/commands/req.js +735 -0
- package/dist/commands/req.js.map +1 -0
- package/dist/commands/stage.d.ts.map +1 -1
- package/dist/commands/stage.js +7 -1
- package/dist/commands/stage.js.map +1 -1
- package/dist/core/config.d.ts +6 -4
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +6 -5
- package/dist/core/config.js.map +1 -1
- package/dist/core/flow-archive/archive-file.d.ts +35 -0
- package/dist/core/flow-archive/archive-file.d.ts.map +1 -0
- package/dist/core/flow-archive/archive-file.js +3 -0
- package/dist/core/flow-archive/archive-file.js.map +1 -0
- package/dist/core/flow-archive/archive-health.d.ts +34 -0
- package/dist/core/flow-archive/archive-health.d.ts.map +1 -0
- package/dist/core/flow-archive/archive-health.js +76 -0
- package/dist/core/flow-archive/archive-health.js.map +1 -0
- package/dist/core/flow-archive/archive-ops.d.ts +68 -0
- package/dist/core/flow-archive/archive-ops.d.ts.map +1 -0
- package/dist/core/flow-archive/archive-ops.js +184 -0
- package/dist/core/flow-archive/archive-ops.js.map +1 -0
- package/dist/core/flow-archive/grouping.d.ts +58 -0
- package/dist/core/flow-archive/grouping.d.ts.map +1 -0
- package/dist/core/flow-archive/grouping.js +153 -0
- package/dist/core/flow-archive/grouping.js.map +1 -0
- package/dist/core/flow-manager.d.ts +67 -9
- package/dist/core/flow-manager.d.ts.map +1 -1
- package/dist/core/flow-manager.js +243 -51
- package/dist/core/flow-manager.js.map +1 -1
- package/dist/core/i18n-data/en.d.ts +1 -0
- package/dist/core/i18n-data/en.d.ts.map +1 -1
- package/dist/core/i18n-data/en.js +174 -4
- package/dist/core/i18n-data/en.js.map +1 -1
- package/dist/core/i18n-data/zh-CN.d.ts +1 -0
- package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
- package/dist/core/i18n-data/zh-CN.js +175 -4
- package/dist/core/i18n-data/zh-CN.js.map +1 -1
- package/dist/core/i18n.js +4 -4
- package/dist/core/i18n.js.map +1 -1
- package/dist/core/managed-region.d.ts +16 -2
- package/dist/core/managed-region.d.ts.map +1 -1
- package/dist/core/managed-region.js +24 -3
- package/dist/core/managed-region.js.map +1 -1
- package/dist/core/metrics.d.ts +2 -0
- package/dist/core/metrics.d.ts.map +1 -1
- package/dist/core/metrics.js +15 -0
- package/dist/core/metrics.js.map +1 -1
- package/dist/core/model-config.d.ts +1 -1
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +2 -2
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/opencode-config.js +1 -1
- package/dist/core/opencode-config.js.map +1 -1
- package/dist/core/pipeline-schema.d.ts +15 -0
- package/dist/core/pipeline-schema.d.ts.map +1 -1
- package/dist/core/pipeline-schema.js +17 -0
- package/dist/core/pipeline-schema.js.map +1 -1
- package/dist/core/plan/path.d.ts +17 -0
- package/dist/core/plan/path.d.ts.map +1 -1
- package/dist/core/plan/path.js +31 -0
- package/dist/core/plan/path.js.map +1 -1
- package/dist/core/plan/stage.d.ts +2 -1
- package/dist/core/plan/stage.d.ts.map +1 -1
- package/dist/core/plan/stage.js +7 -2
- package/dist/core/plan/stage.js.map +1 -1
- package/dist/core/req/aggregate.d.ts +62 -0
- package/dist/core/req/aggregate.d.ts.map +1 -0
- package/dist/core/req/aggregate.js +134 -0
- package/dist/core/req/aggregate.js.map +1 -0
- package/dist/core/req/body-file.d.ts +16 -0
- package/dist/core/req/body-file.d.ts.map +1 -0
- package/dist/core/req/body-file.js +84 -0
- package/dist/core/req/body-file.js.map +1 -0
- package/dist/core/req/requirement-ops.d.ts +116 -0
- package/dist/core/req/requirement-ops.d.ts.map +1 -0
- package/dist/core/req/requirement-ops.js +395 -0
- package/dist/core/req/requirement-ops.js.map +1 -0
- package/dist/core/req/types.d.ts +46 -0
- package/dist/core/req/types.d.ts.map +1 -0
- package/dist/core/req/types.js +7 -0
- package/dist/core/req/types.js.map +1 -0
- package/dist/core/runtime-log.d.ts +18 -0
- package/dist/core/runtime-log.d.ts.map +1 -1
- package/dist/core/runtime-log.js +72 -0
- package/dist/core/runtime-log.js.map +1 -1
- package/dist/core/session.d.ts +24 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +107 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/setup.js +4 -4
- package/dist/core/template-loader.d.ts.map +1 -1
- package/dist/core/template-loader.js +3160 -6571
- package/dist/core/template-loader.js.map +1 -1
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +770 -26
- package/dist/core/update.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/flow.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
1544
|
-
|
|
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(
|
|
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
|
-
|
|
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());
|