openfeel 1.1.6 → 1.2.0
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 +25 -6
- package/README.md +17 -2
- package/README.zh-CN.md +25 -6
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +85 -4
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/flow.d.ts +8 -0
- package/dist/commands/flow.d.ts.map +1 -1
- package/dist/commands/flow.js +447 -15
- package/dist/commands/flow.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +71 -7
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/req.d.ts +14 -0
- package/dist/commands/req.d.ts.map +1 -0
- package/dist/commands/req.js +562 -0
- package/dist/commands/req.js.map +1 -0
- package/dist/commands/stage.d.ts.map +1 -1
- package/dist/commands/stage.js +56 -2
- package/dist/commands/stage.js.map +1 -1
- package/dist/core/config.d.ts +17 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +21 -6
- 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 +78 -14
- package/dist/core/flow-manager.d.ts.map +1 -1
- package/dist/core/flow-manager.js +311 -55
- 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 +159 -3
- 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 +161 -3
- 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/pipeline-schema.d.ts +5 -0
- package/dist/core/pipeline-schema.d.ts.map +1 -1
- package/dist/core/pipeline-schema.js +5 -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/scheme.d.ts +22 -0
- package/dist/core/plan/scheme.d.ts.map +1 -1
- package/dist/core/plan/scheme.js +71 -2
- package/dist/core/plan/scheme.js.map +1 -1
- package/dist/core/plan/stage.d.ts +2 -0
- package/dist/core/plan/stage.d.ts.map +1 -1
- package/dist/core/plan/stage.js +8 -0
- package/dist/core/plan/stage.js.map +1 -1
- package/dist/core/req/aggregate.d.ts +61 -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 +80 -0
- package/dist/core/req/body-file.js.map +1 -0
- package/dist/core/req/requirement-ops.d.ts +74 -0
- package/dist/core/req/requirement-ops.d.ts.map +1 -0
- package/dist/core/req/requirement-ops.js +290 -0
- package/dist/core/req/requirement-ops.js.map +1 -0
- package/dist/core/req/types.d.ts +40 -0
- package/dist/core/req/types.d.ts.map +1 -0
- package/dist/core/req/types.js +13 -0
- package/dist/core/req/types.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 +649 -65
- package/dist/core/template-loader.js.map +1 -1
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +131 -15
- package/dist/core/update.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/flow.js
CHANGED
|
@@ -3,11 +3,18 @@ import { existsSync, copyFileSync, readFileSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
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
|
+
import { validateStageId, suggestStageId, normalizeStageId, findStageStatusPath, isArchivedVersionOf } from '../core/plan/path.js';
|
|
7
|
+
import { getConfigFieldLegalValues, setConfigValue } from '../core/config.js';
|
|
8
|
+
import { parse as parseYaml } from 'yaml';
|
|
7
9
|
import { MetricsStore } from '../core/metrics.js';
|
|
8
10
|
import { addReviewEntry } from '../core/view/entry.js';
|
|
9
11
|
import { handleAddStageError, handleConcurrentConflict } from './shared/errors.js';
|
|
10
12
|
import { t, getCliLang } from '../core/i18n.js';
|
|
13
|
+
import { ARCHIVE_SCHEMA_VERSION } from '../core/flow-archive/archive-file.js';
|
|
14
|
+
import { listArchivableCandidates, listBlockingOpenRevsForVersion, splitForArchive } from '../core/flow-archive/grouping.js';
|
|
15
|
+
import { normalizeArchiveVersion, writeArchiveFile, addArchivedVersion, applyArchivePipelineDisposition, listArchiveVersions, readArchiveFile } from '../core/flow-archive/archive-ops.js';
|
|
16
|
+
import { archiveHealthItems } from '../core/flow-archive/archive-health.js';
|
|
17
|
+
import { requirementHealthItems } from '../core/req/aggregate.js'; // 需求对账项(stage-83 T3)
|
|
11
18
|
export function registerFlowCommand(program) {
|
|
12
19
|
const flow = program
|
|
13
20
|
.command('flow')
|
|
@@ -341,22 +348,58 @@ export function registerFlowCommand(program) {
|
|
|
341
348
|
.command('phases')
|
|
342
349
|
.description('列出全部合法 phase 及其流转映射(自描述)')
|
|
343
350
|
.option('--json', 'Output { phases, transitions, advanceAccepted, schemaVersion } as JSON')
|
|
351
|
+
.option('--mode <mode>', '显式指定工作流模式(full | fast;缺省经 --stage / 当前阶段派生)')
|
|
352
|
+
.option('--stage <id>', '按指定阶段的模式展示转移表(缺省 = 当前阶段)')
|
|
344
353
|
.action((options) => {
|
|
345
|
-
const
|
|
354
|
+
const projectPath = process.cwd();
|
|
355
|
+
const lang = getCliLang(projectPath);
|
|
346
356
|
const mgr = createManager();
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
357
|
+
// ① --mode 显式值域校验(schema 派生;非法 → exit 1,不进入只读取值)
|
|
358
|
+
let explicitMode;
|
|
359
|
+
if (options.mode !== undefined) {
|
|
360
|
+
const allowed = getConfigFieldLegalValues('workflow_mode') ?? [];
|
|
361
|
+
if (!allowed.includes(options.mode)) {
|
|
362
|
+
console.error(t('stage.set.invalidValueTmpl', lang, { field: '工作流模式', value: options.mode, allowed: allowed.join(', ') }));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
explicitMode = options.mode;
|
|
367
|
+
}
|
|
368
|
+
// ② --stage 校验(格式 + 存在性):先于任何只读取值
|
|
369
|
+
let stageArg;
|
|
370
|
+
if (options.stage !== undefined) {
|
|
371
|
+
const v = validateStageId(options.stage);
|
|
372
|
+
if (!v.ok) {
|
|
373
|
+
console.error(t('common.stageIdInvalidTmpl', lang, { input: options.stage }));
|
|
374
|
+
console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, options.stage) }));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
stageArg = normalizeStageId(options.stage) ?? options.stage;
|
|
379
|
+
const known = new Set(Object.keys(mgr.isLoaded() ? mgr.getData().stages : {}).map((k) => normalizeStageId(k) ?? k));
|
|
380
|
+
if (!known.has(stageArg) && !findStageStatusPath(projectPath, stageArg)) {
|
|
381
|
+
console.error(t('common.stageIdInvalidTmpl', lang, { input: options.stage }));
|
|
382
|
+
console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, options.stage) }));
|
|
383
|
+
process.exit(1);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// ③ 单点派生:显式 --mode 优先,否则经 --stage / 当前阶段级联解析
|
|
388
|
+
const resolved = mgr.resolveStageWorkflowMode(stageArg);
|
|
389
|
+
const derivedMode = explicitMode ?? resolved.value;
|
|
390
|
+
// ④ 显式传 mode(消除无参调用;访问器默认形参保留)
|
|
391
|
+
const transitions = mgr.getPipelineTransitions(derivedMode);
|
|
392
|
+
// 运行时 vs 内置默认 transitions 差异(T19:使漂移可见,不补组合键;同模式对比)
|
|
393
|
+
const defaults = mgr.getDefaultTransitions(derivedMode);
|
|
351
394
|
const missingKeys = Object.keys(defaults).filter((k) => !(k in transitions));
|
|
352
395
|
const extraKeys = Object.keys(transitions).filter((k) => !(k in defaults));
|
|
353
396
|
const changedKeys = Object.keys(defaults).filter((k) => k in transitions && JSON.stringify(defaults[k]) !== JSON.stringify(transitions[k]));
|
|
354
397
|
const transitionsDiff = { missing: missingKeys, extra: extraKeys, changed: changedKeys };
|
|
398
|
+
const phases = mgr.getPipelinePhases();
|
|
355
399
|
if (options.json) {
|
|
356
400
|
// advanceAccepted = 内置 15 phase(flow advance 的推进白名单),与 phases(运行时存在视图,可含自定义)显式区分
|
|
357
|
-
// 既有三字段(phases/transitions/advanceAccepted)+ transitionsDiff
|
|
358
|
-
|
|
359
|
-
console.log(JSON.stringify({ schemaVersion: 1, phases, transitions, advanceAccepted: [...PIPELINE_PHASES], transitionsDiff }, null, 2));
|
|
401
|
+
// 既有三字段(phases/transitions/advanceAccepted)+ transitionsDiff 逐字保留;schemaVersion 与 workflowMode 追加于末位(B1-6 / stage-75 op-002)
|
|
402
|
+
console.log(JSON.stringify({ schemaVersion: 1, phases, transitions, advanceAccepted: [...PIPELINE_PHASES], transitionsDiff, workflowMode: derivedMode }, null, 2));
|
|
360
403
|
return;
|
|
361
404
|
}
|
|
362
405
|
console.log(t('flow.phases.title', lang));
|
|
@@ -365,6 +408,9 @@ export function registerFlowCommand(program) {
|
|
|
365
408
|
console.log(` - ${p}`);
|
|
366
409
|
}
|
|
367
410
|
console.log('');
|
|
411
|
+
// 模式分节行(stage-75 op-002:模式 + 来源 + 查看指引)
|
|
412
|
+
console.log(t('flow.mode.lineTmpl', lang, { mode: derivedMode, source: explicitMode ? '--mode' : resolved.source }));
|
|
413
|
+
console.log(t('flow.phases.modeHint', lang));
|
|
368
414
|
console.log(t('flow.phases.transitionsLabel', lang) + ':');
|
|
369
415
|
for (const [key, targets] of Object.entries(transitions)) {
|
|
370
416
|
console.log(` ${key} → [${targets.join(', ')}]`);
|
|
@@ -391,6 +437,62 @@ export function registerFlowCommand(program) {
|
|
|
391
437
|
}
|
|
392
438
|
console.log(t('flow.phases.hint', lang));
|
|
393
439
|
});
|
|
440
|
+
// flow mode — 当前工作流模式与来源(只读)
|
|
441
|
+
flow
|
|
442
|
+
.command('mode')
|
|
443
|
+
.description('显示当前工作流模式与来源(只读)')
|
|
444
|
+
.option('--stage <id>', '目标阶段(缺省 = 当前阶段)')
|
|
445
|
+
.option('--json', 'Output as JSON')
|
|
446
|
+
.action((options) => {
|
|
447
|
+
const projectPath = process.cwd();
|
|
448
|
+
const lang = getCliLang(projectPath);
|
|
449
|
+
const mgr = createManager();
|
|
450
|
+
let key;
|
|
451
|
+
if (options.stage !== undefined) {
|
|
452
|
+
const v = validateStageId(options.stage);
|
|
453
|
+
if (!v.ok) {
|
|
454
|
+
console.error(t('common.stageIdInvalidTmpl', lang, { input: options.stage }));
|
|
455
|
+
console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, options.stage) }));
|
|
456
|
+
process.exit(1);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
key = normalizeStageId(options.stage) ?? options.stage;
|
|
460
|
+
// 存在性:已注册(flow.json)或有 status.md;二者皆无 → exit 1(防 typo 静默落 config 层)
|
|
461
|
+
const known = new Set(Object.keys(mgr.isLoaded() ? mgr.getData().stages : {}).map((k) => normalizeStageId(k) ?? k));
|
|
462
|
+
if (!known.has(key) && !findStageStatusPath(projectPath, key)) {
|
|
463
|
+
console.error(t('common.stageIdInvalidTmpl', lang, { input: options.stage }));
|
|
464
|
+
console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, options.stage) }));
|
|
465
|
+
process.exit(1);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
// 缺省 = 当前阶段;无 current 时 undefined(级联仍可解析 config/builtin)
|
|
471
|
+
key = mgr.getData()?.pipeline?.current?.stage || undefined;
|
|
472
|
+
}
|
|
473
|
+
// 唯一解析入口(禁第二套来源判定)
|
|
474
|
+
const resolved = mgr.resolveStageWorkflowMode(key);
|
|
475
|
+
const modes = {
|
|
476
|
+
full: mgr.getPipelineTransitions('full'),
|
|
477
|
+
fast: mgr.getPipelineTransitions('fast'),
|
|
478
|
+
};
|
|
479
|
+
if (options.json) {
|
|
480
|
+
// 未初始化亦保纯 JSON 单文档(stage 空串 + 无 stage 域级联解析值)
|
|
481
|
+
console.log(JSON.stringify({ schemaVersion: 1, stage: key ?? '', mode: resolved.value, source: resolved.source, modes }, null, 2));
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (!mgr.isLoaded()) {
|
|
485
|
+
console.log(t('common.noInit', lang));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
console.log(t('flow.mode.lineTmpl', lang, { mode: resolved.value, source: resolved.source }));
|
|
489
|
+
// 阶段 phase 已知时补可达目标行(同源 getAvailablePhases)
|
|
490
|
+
const phase = key ? mgr.getData()?.stages[key]?.phase : undefined;
|
|
491
|
+
if (key && phase) {
|
|
492
|
+
console.log(t('flow.mode.targetsLabel', lang, { stage: key, phase, targets: mgr.getAvailablePhases(key).join(', ') }));
|
|
493
|
+
}
|
|
494
|
+
// 只读保证:不 save()、不 appendLog、不写任何文件
|
|
495
|
+
});
|
|
394
496
|
// flow stage — 阶段管理子命令组
|
|
395
497
|
const stageCmd = flow
|
|
396
498
|
.command('stage')
|
|
@@ -528,7 +630,12 @@ export function registerFlowCommand(program) {
|
|
|
528
630
|
.filter(Boolean);
|
|
529
631
|
// 悬空依赖:命令层强制校验(核心层仅 warn),非法一律 exit 1 且不写盘
|
|
530
632
|
const known = new Set(Object.keys(data.stages).map((k) => normalizeStageId(k) ?? k));
|
|
531
|
-
|
|
633
|
+
// 已归档依赖(显式版本前缀 ∧ 版本 ∈ archived_versions)→ 视为已满足(同 plan.ts 单一口径;恒传原始 d)
|
|
634
|
+
const archivedOk = deps.filter((d) => !known.has(normalizeStageId(d) ?? d) && isArchivedVersionOf(d, data.meta?.archived_versions));
|
|
635
|
+
const invalid = deps.filter((d) => !known.has(normalizeStageId(d) ?? d) && !isArchivedVersionOf(d, data.meta?.archived_versions));
|
|
636
|
+
if (archivedOk.length > 0) {
|
|
637
|
+
console.log(t('stage.set.archivedDepsTmpl', lang, { deps: archivedOk.join(', ') }));
|
|
638
|
+
}
|
|
532
639
|
if (invalid.length > 0) {
|
|
533
640
|
console.error(t('stage.set.depsDanglingTmpl', lang, { deps: invalid.join(', ') }));
|
|
534
641
|
process.exit(1);
|
|
@@ -611,7 +718,7 @@ export function registerFlowCommand(program) {
|
|
|
611
718
|
.command('advance')
|
|
612
719
|
.description('推进流水线阶段')
|
|
613
720
|
.option('--op <id>', '操作 ID(如 stage-01.op-001),仅用于日志/展示')
|
|
614
|
-
.requiredOption('--to <phase>', '目标阶段(如 exec_running)。合法 phase 与转移表:openfeel flow phases')
|
|
721
|
+
.requiredOption('--to <phase>', '目标阶段(如 exec_running)。合法 phase 与转移表:openfeel flow phases;查看当前模式:openfeel flow mode')
|
|
615
722
|
.option('--stage <id>', '阶段 ID(如 stage-03),必须指定')
|
|
616
723
|
.option('--force', '强制执行(跳过非法 phase 校验和阶段跳跃检查,但不可绕过 REV 阻塞检查)')
|
|
617
724
|
.option('--dry-run', '仅验证不执行修改(预览输出)。与 --force 组合时跳过校验但仍不执行修改')
|
|
@@ -1416,6 +1523,253 @@ export function registerFlowCommand(program) {
|
|
|
1416
1523
|
process.exit(1);
|
|
1417
1524
|
}
|
|
1418
1525
|
});
|
|
1526
|
+
// flow archive <version> — flow.json 版本分片归档(裁定 O2;与顶层 openfeel archive〔阶段摘要〕职责分离,裁定 O3)
|
|
1527
|
+
const archiveCmd = flow
|
|
1528
|
+
.command('archive')
|
|
1529
|
+
.description('flow.json 版本分片归档(按版本号归档已收官历史)')
|
|
1530
|
+
.argument('<version>', '版本号(如 v1.1.7;前缀 v 可省略)')
|
|
1531
|
+
.option('--dry-run', '仅预览(判据门照常校验;零写盘)')
|
|
1532
|
+
.option('--json', '以 JSON 输出 (machine-readable, single JSON document)')
|
|
1533
|
+
.action((versionInput, options) => {
|
|
1534
|
+
const projectPath = process.cwd();
|
|
1535
|
+
const lang = getCliLang(projectPath);
|
|
1536
|
+
const mgr = createManager();
|
|
1537
|
+
// --json 失败出口统一:纯 JSON 单文档 + exit 1(不混人类文案、不走 stderr)
|
|
1538
|
+
const failJson = (payload) => {
|
|
1539
|
+
console.log(JSON.stringify({ schemaVersion: 1, ...payload }, null, 2));
|
|
1540
|
+
process.exitCode = 1;
|
|
1541
|
+
};
|
|
1542
|
+
if (!mgr.isLoaded()) {
|
|
1543
|
+
if (options.json) {
|
|
1544
|
+
failJson({ error: 'no-init' });
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
console.error(t('common.errorNoInit', lang));
|
|
1548
|
+
process.exitCode = 1;
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
// 版本归一(REV-79-004):去 v 再加 v;非法 → exit 1 零写盘
|
|
1552
|
+
const version = normalizeArchiveVersion(versionInput);
|
|
1553
|
+
if (!version) {
|
|
1554
|
+
if (options.json) {
|
|
1555
|
+
failJson({ error: 'invalid-version', input: versionInput });
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
console.error(t('flow.archive.invalidVersionTmpl', lang, { input: versionInput }));
|
|
1559
|
+
process.exitCode = 1;
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const data = mgr.getData();
|
|
1563
|
+
// 版本存在性门(前置;REV-79-001):无候选(不存在/已归档)→ 拒绝零写盘;覆盖写仅限活跃集仍含该版本条目
|
|
1564
|
+
const candidate = listArchivableCandidates(data).find((c) => c.version === version);
|
|
1565
|
+
if (!candidate) {
|
|
1566
|
+
if (options.json) {
|
|
1567
|
+
failJson({ error: 'version-not-active', version });
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
console.error(t('flow.archive.versionNotActiveTmpl', lang, { version }));
|
|
1571
|
+
process.exitCode = 1;
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
// 判据门:全 done(candidate.allDone 空集守卫)+ 无 blocking open REV(全仓唯一谓词,REV-79-002)
|
|
1575
|
+
const notDone = candidate.stages.filter((id) => data.stages[id]?.phase !== 'done');
|
|
1576
|
+
const blockingOpen = listBlockingOpenRevsForVersion(data, version);
|
|
1577
|
+
if (!candidate.allDone || blockingOpen.length > 0) {
|
|
1578
|
+
if (options.json) {
|
|
1579
|
+
failJson({ error: 'gate-rejected', version, notDone, blockingOpen: blockingOpen.map((r) => r.id) });
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (notDone.length > 0) {
|
|
1583
|
+
console.error(t('flow.archive.gateNotDoneTitleTmpl', lang, { version, n: String(notDone.length) }));
|
|
1584
|
+
for (const id of notDone) {
|
|
1585
|
+
console.error(t('flow.archive.gateItemTmpl', lang, { item: id + ' [' + (data.stages[id]?.phase ?? '') + ']' }));
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (blockingOpen.length > 0) {
|
|
1589
|
+
console.error(t('flow.archive.gateBlockingRevTitleTmpl', lang, { version, n: String(blockingOpen.length) }));
|
|
1590
|
+
for (const r of blockingOpen) {
|
|
1591
|
+
console.error(t('flow.archive.gateItemTmpl', lang, { item: r.id + ': ' + r.title + ' (priority=' + r.priority + ')' }));
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
console.error(t('flow.archive.gateRejected', lang));
|
|
1595
|
+
process.exitCode = 1;
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
// 切分(纯函数;禁命令层第二套解析/切分)
|
|
1599
|
+
const { archive, remainder } = splitForArchive(data, version);
|
|
1600
|
+
const counts = archive.counts;
|
|
1601
|
+
// 装配(REV-79-009):SplitResult.archive 不含 schemaVersion / archived_at
|
|
1602
|
+
const file = { ...archive, schemaVersion: ARCHIVE_SCHEMA_VERSION, archived_at: new Date().toISOString() };
|
|
1603
|
+
// --dry-run:纯内存推演(不写归档文件 / 不 addArchivedVersion / 不 appendLog / 不 save)——四写盘点零触碰
|
|
1604
|
+
if (options.dryRun) {
|
|
1605
|
+
// 深拷 pipeline(REV-79-015):处置 helper 会就地改 current/phase,预览不得污染 live data
|
|
1606
|
+
const previewData = {
|
|
1607
|
+
...data,
|
|
1608
|
+
stages: remainder.stages,
|
|
1609
|
+
reviews: remainder.reviews,
|
|
1610
|
+
log: remainder.log,
|
|
1611
|
+
pipeline: { ...data.pipeline, current: { ...data.pipeline.current } },
|
|
1612
|
+
};
|
|
1613
|
+
const preview = applyArchivePipelineDisposition(previewData, version);
|
|
1614
|
+
if (options.json) {
|
|
1615
|
+
console.log(JSON.stringify({ schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log, dryRun: true }, null, 2));
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
console.log(t('flow.archive.previewTmpl', lang, { version, stages: String(counts.stages), reviews: String(counts.reviews), log: String(counts.log) }));
|
|
1619
|
+
console.log(t('flow.archive.pipelineTmpl', lang, {
|
|
1620
|
+
current: preview.currentCleared ? t('common.none', lang) : (previewData.pipeline.current.stage || t('common.none', lang)),
|
|
1621
|
+
phase: preview.phase,
|
|
1622
|
+
}));
|
|
1623
|
+
console.log(t('flow.archive.previewNoWrite', lang));
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
// ① 先写归档文件(新增;独立锁 flow-archive,与 flow 锁不嵌套)
|
|
1627
|
+
try {
|
|
1628
|
+
writeArchiveFile(projectPath, file);
|
|
1629
|
+
}
|
|
1630
|
+
catch (err) {
|
|
1631
|
+
// 写盘失败(REV-79-013):内存未改动(移除/索引/审计均在其后),无需回滚;exit 1 零 flow.json 变更
|
|
1632
|
+
if (options.json) {
|
|
1633
|
+
failJson({ error: 'archive-write-failed', version });
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
console.error(t('common.errorTmpl', lang, { msg: err instanceof Error ? err.message : String(err) }));
|
|
1637
|
+
process.exitCode = 1;
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
// ② 内存移除(stages / reviews / log 三段)
|
|
1641
|
+
data.stages = remainder.stages;
|
|
1642
|
+
data.reviews = remainder.reviews;
|
|
1643
|
+
data.log = remainder.log;
|
|
1644
|
+
// ③ 索引(幂等去重;纯内存)
|
|
1645
|
+
const added = addArchivedVersion(data, version);
|
|
1646
|
+
// ④ pipeline 处置(纯内存;空集 → active,与 flow-manager.ts:1495-1496 守卫同效)
|
|
1647
|
+
const disposed = applyArchivePipelineDisposition(data, version);
|
|
1648
|
+
// ⑤ 审计(须晚于 ②,记录在归档后 flow.json——不混入被归档段)
|
|
1649
|
+
mgr.appendLog({ time: '', agent: 'cli', action: 'archive_flow_version', detail: { version, stages: counts.stages, reviews: counts.reviews, log: counts.log } });
|
|
1650
|
+
// ⑥ 后 save()(顺序不变量:归档文件写成功之前不改 flow.json)
|
|
1651
|
+
try {
|
|
1652
|
+
mgr.save();
|
|
1653
|
+
}
|
|
1654
|
+
catch (err) {
|
|
1655
|
+
if (isFlowConcurrentError(err)) {
|
|
1656
|
+
handleConcurrentConflict(err, lang); // exit 2(归档文件已落盘;重跑幂等收敛)
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
console.error(t('common.errorTmpl', lang, { msg: err instanceof Error ? err.message : String(err) }));
|
|
1660
|
+
process.exitCode = 1;
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (options.json) {
|
|
1664
|
+
console.log(JSON.stringify({ schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log, dryRun: false }, null, 2));
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (added) {
|
|
1668
|
+
console.log(t('flow.archive.doneTmpl', lang, { version, stages: String(counts.stages), reviews: String(counts.reviews), log: String(counts.log) }));
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
console.log(t('flow.archive.rearchivedTmpl', lang, { version }));
|
|
1672
|
+
}
|
|
1673
|
+
console.log(t('flow.archive.pipelineTmpl', lang, {
|
|
1674
|
+
current: disposed.currentCleared ? t('common.none', lang) : (data.pipeline.current.stage || t('common.none', lang)),
|
|
1675
|
+
phase: disposed.phase,
|
|
1676
|
+
}));
|
|
1677
|
+
});
|
|
1678
|
+
// flow archive list — 列出已归档版本及计数(只读;引擎字典序)
|
|
1679
|
+
archiveCmd
|
|
1680
|
+
.command('list')
|
|
1681
|
+
.description('列出已归档版本及计数(只读)')
|
|
1682
|
+
.option('--json', '以 JSON 输出 (machine-readable, single JSON document)')
|
|
1683
|
+
.action((options, command) => {
|
|
1684
|
+
const projectPath = process.cwd();
|
|
1685
|
+
const lang = getCliLang(projectPath);
|
|
1686
|
+
// 父命令 archive 亦声明 --json,Commander 派发子命令前会先由父命令消费该标志
|
|
1687
|
+
// → 合并父级 opts(否则 `flow archive list --json` 的 --json 不可达;REV-79-018)
|
|
1688
|
+
const json = options.json === true || command.parent?.opts().json === true;
|
|
1689
|
+
// 引擎字典序(零命令层第二套排序);逐文件 counts 经读侧复用(损坏 → null)
|
|
1690
|
+
const rows = listArchiveVersions(projectPath).map((v) => ({
|
|
1691
|
+
version: v,
|
|
1692
|
+
counts: readArchiveFile(projectPath, v)?.counts ?? null,
|
|
1693
|
+
}));
|
|
1694
|
+
if (json) {
|
|
1695
|
+
console.log(JSON.stringify({ schemaVersion: 1, versions: rows }, null, 2));
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (rows.length === 0) {
|
|
1699
|
+
console.log(t('flow.archive.list.empty', lang));
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
console.log(t('flow.archive.list.title', lang));
|
|
1703
|
+
for (const r of rows) {
|
|
1704
|
+
if (r.counts) {
|
|
1705
|
+
console.log(t('flow.archive.list.itemTmpl', lang, {
|
|
1706
|
+
version: r.version, stages: String(r.counts.stages), reviews: String(r.counts.reviews), log: String(r.counts.log),
|
|
1707
|
+
}));
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
console.log(t('flow.archive.list.unreadableTmpl', lang, { version: r.version }));
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
// 只读保证:不 save()、不 appendLog、不读 flow.json
|
|
1714
|
+
});
|
|
1715
|
+
// flow archive show <version> — 查看指定归档版本(只读)
|
|
1716
|
+
archiveCmd
|
|
1717
|
+
.command('show')
|
|
1718
|
+
.description('查看指定归档版本(只读)')
|
|
1719
|
+
.argument('<version>', '版本号(如 v1.1.7;前缀 v 可省略)')
|
|
1720
|
+
.option('--json', '以 JSON 输出(全量,含 log / reviews)')
|
|
1721
|
+
.action((versionInput, options, command) => {
|
|
1722
|
+
const projectPath = process.cwd();
|
|
1723
|
+
const lang = getCliLang(projectPath);
|
|
1724
|
+
// 同 list:合并父级 --json(REV-79-018)
|
|
1725
|
+
const json = options.json === true || command.parent?.opts().json === true;
|
|
1726
|
+
const available = listArchiveVersions(projectPath);
|
|
1727
|
+
// 版本归一(前缀兼容):非法 → exit 1 零写盘
|
|
1728
|
+
const version = normalizeArchiveVersion(versionInput);
|
|
1729
|
+
if (!version) {
|
|
1730
|
+
if (json) {
|
|
1731
|
+
console.log(JSON.stringify({ schemaVersion: 1, error: 'invalid-version', input: versionInput, available }, null, 2));
|
|
1732
|
+
process.exitCode = 1;
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
console.error(t('flow.archive.invalidVersionTmpl', lang, { input: versionInput }));
|
|
1736
|
+
process.exitCode = 1;
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const file = readArchiveFile(projectPath, version);
|
|
1740
|
+
if (!file) {
|
|
1741
|
+
if (json) {
|
|
1742
|
+
console.log(JSON.stringify({ schemaVersion: 1, error: 'not-found', version, available }, null, 2));
|
|
1743
|
+
process.exitCode = 1;
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
console.error(t('flow.archive.show.notFoundTmpl', lang, {
|
|
1747
|
+
version, versions: available.length > 0 ? available.join(', ') : t('common.none', lang),
|
|
1748
|
+
}));
|
|
1749
|
+
process.exitCode = 1;
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
// --json = 全量载荷(ArchiveFile 原文自带 schemaVersion:1,含 log / reviews)——显式请求才取全量(REV-79-003)
|
|
1753
|
+
if (json) {
|
|
1754
|
+
console.log(JSON.stringify(file, null, 2));
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
console.log(t('flow.archive.show.titleTmpl', lang, { version, at: typeof file.archived_at === 'string' ? file.archived_at : '' }));
|
|
1758
|
+
console.log(t('flow.archive.show.countsTmpl', lang, {
|
|
1759
|
+
stages: String(file.counts.stages), reviews: String(file.counts.reviews), log: String(file.counts.log),
|
|
1760
|
+
}));
|
|
1761
|
+
console.log(t('flow.archive.show.stagesLabel', lang));
|
|
1762
|
+
const entries = Object.entries(file.stages ?? {});
|
|
1763
|
+
if (entries.length === 0) {
|
|
1764
|
+
console.log(t('common.noData', lang));
|
|
1765
|
+
}
|
|
1766
|
+
else {
|
|
1767
|
+
for (const [id, stage] of entries) {
|
|
1768
|
+
console.log(t('flow.archive.show.stageLineTmpl', lang, { stage: id, phase: stage?.phase ?? '' }));
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
console.log(t('flow.archive.show.fullHint', lang));
|
|
1772
|
+
});
|
|
1419
1773
|
// flow health [--quick]
|
|
1420
1774
|
flow
|
|
1421
1775
|
.command('health')
|
|
@@ -1427,6 +1781,15 @@ export function registerFlowCommand(program) {
|
|
|
1427
1781
|
.action((options) => {
|
|
1428
1782
|
const lang = getCliLang(process.cwd());
|
|
1429
1783
|
const mgr = createManager();
|
|
1784
|
+
const quick = options.quick ?? false;
|
|
1785
|
+
// 归档一致性检查项(T2):warn-only;--quick 跳过;flow.json 未加载 → 跳过(与既有检查降级一致)
|
|
1786
|
+
const archiveItems = !quick && mgr.isLoaded()
|
|
1787
|
+
? archiveHealthItems({ projectPath: process.cwd(), data: mgr.getData() })
|
|
1788
|
+
: [];
|
|
1789
|
+
// 需求对账检查项(stage-83 T3):warn-only(悬空关联 / done 漂移);--quick 跳过;未加载 → 跳过
|
|
1790
|
+
const reqItems = !quick && mgr.isLoaded()
|
|
1791
|
+
? requirementHealthItems({ data: mgr.getData(), lang })
|
|
1792
|
+
: [];
|
|
1430
1793
|
// B2-2:--fix 状态对账(仅回写 status.md「状态」字段;--dry-run 预览不写盘)
|
|
1431
1794
|
let reconciled;
|
|
1432
1795
|
if (options.fix) {
|
|
@@ -1443,8 +1806,14 @@ export function registerFlowCommand(program) {
|
|
|
1443
1806
|
}
|
|
1444
1807
|
// B1-3:--json 分支纯 JSON 单文档;退出码语义不变(有 fail → 非 0)
|
|
1445
1808
|
if (options.json) {
|
|
1446
|
-
const report = mgr.getHealthReport(
|
|
1447
|
-
|
|
1809
|
+
const report = mgr.getHealthReport(quick);
|
|
1810
|
+
// 合并追加归档一致性项(warn-only,恒无 fail → ok / 退出码不变)
|
|
1811
|
+
const items = [
|
|
1812
|
+
...report.items,
|
|
1813
|
+
...archiveItems.map((i) => ({ name: i.section, status: i.status, detail: i.message })),
|
|
1814
|
+
...reqItems.map((i) => ({ name: i.section, status: i.status, detail: i.message })),
|
|
1815
|
+
];
|
|
1816
|
+
const payload = { schemaVersion: 1, ok: report.ok, items };
|
|
1448
1817
|
// 与 op-001 的 health --json 合并字段(--fix 时追加 reconciled)
|
|
1449
1818
|
if (reconciled) {
|
|
1450
1819
|
payload.reconciled = reconciled;
|
|
@@ -1481,7 +1850,7 @@ export function registerFlowCommand(program) {
|
|
|
1481
1850
|
console.log('');
|
|
1482
1851
|
}
|
|
1483
1852
|
console.log(t('flow.health.title', lang) + '\n');
|
|
1484
|
-
const result = mgr.healthCheck(
|
|
1853
|
+
const result = mgr.healthCheck(quick);
|
|
1485
1854
|
// 图标映射
|
|
1486
1855
|
const icon = (status) => {
|
|
1487
1856
|
if (status === 'pass')
|
|
@@ -1490,7 +1859,8 @@ export function registerFlowCommand(program) {
|
|
|
1490
1859
|
return '⚠️ ';
|
|
1491
1860
|
return '❌';
|
|
1492
1861
|
};
|
|
1493
|
-
|
|
1862
|
+
// 合并追加归档一致性项(warn-only;--quick 时 archiveItems 恒空)
|
|
1863
|
+
for (const item of [...result.items, ...archiveItems, ...reqItems]) {
|
|
1494
1864
|
console.log(`${icon(item.status)} ${item.section}: ${item.message}`);
|
|
1495
1865
|
}
|
|
1496
1866
|
console.log('');
|
|
@@ -1641,6 +2011,32 @@ export function registerFlowCommand(program) {
|
|
|
1641
2011
|
console.log(t('flow.wizard.unavailable', lang));
|
|
1642
2012
|
return;
|
|
1643
2013
|
}
|
|
2014
|
+
// stage-75 T4:首次选型 + 项目级记忆(TTY + 阶段无字段 + 项目未显式设默认 → 询问一次)
|
|
2015
|
+
{
|
|
2016
|
+
const modeResolved = mgr.resolveStageWorkflowMode(currentStage);
|
|
2017
|
+
const stageHasField = modeResolved.source === 'status.md';
|
|
2018
|
+
const projectHasDefault = hasExplicitProjectDefault(process.cwd(), 'workflow_mode');
|
|
2019
|
+
if (shouldPromptWorkflowMode(stageHasField, projectHasDefault) === 'prompt') {
|
|
2020
|
+
const chosen = (await select({
|
|
2021
|
+
message: t('flow.wizard.modePrompt', lang),
|
|
2022
|
+
choices: [
|
|
2023
|
+
{ name: t('flow.wizard.modeFull', lang), value: 'full' },
|
|
2024
|
+
{ name: t('flow.wizard.modeFast', lang), value: 'fast' },
|
|
2025
|
+
],
|
|
2026
|
+
}));
|
|
2027
|
+
const statusPath = findStageStatusPath(process.cwd(), currentStage);
|
|
2028
|
+
if (statusPath) {
|
|
2029
|
+
const r = mgr.insertStatusField(statusPath, currentStage, '工作流模式', chosen);
|
|
2030
|
+
if (r === 'no-anchor') {
|
|
2031
|
+
// 错误路径:手工破坏文件(骨架必含锚点)→ 警告而非阻断(D5)
|
|
2032
|
+
console.error(t('stage.set.noAnchorTmpl', lang, { stageId: currentStage, field: '工作流模式' }));
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
// 记忆写入仅 defaults.workflow_mode(增量写、保注释),不覆盖其它 defaults 键
|
|
2036
|
+
setConfigValue(process.cwd(), 'workflow_mode', chosen);
|
|
2037
|
+
console.log(t('flow.wizard.modeRememberedTmpl', lang, { mode: chosen }));
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
1644
2040
|
const stagePhase = data.stages[currentStage].phase;
|
|
1645
2041
|
// 显示当前状态
|
|
1646
2042
|
console.log('\n' + t('flow.wizard.statusHeader', lang));
|
|
@@ -1720,6 +2116,42 @@ export function registerFlowCommand(program) {
|
|
|
1720
2116
|
}
|
|
1721
2117
|
});
|
|
1722
2118
|
}
|
|
2119
|
+
/**
|
|
2120
|
+
* 「是否询问工作流模式」决策矩阵(stage-75 T4,D4)。
|
|
2121
|
+
* 仅当阶段 status.md 无「工作流模式」字段 **且** 项目 config.yaml 未显式设 `defaults.workflow_mode` 时询问;
|
|
2122
|
+
* 其余 3 态(有字段 / 有项目默认 / 两者皆有)一律 skip(保持既有推进流不变)。
|
|
2123
|
+
* @param stageHasField 阶段 status.md 是否已有「工作流模式」字段
|
|
2124
|
+
* @param projectHasDefault 项目 config.yaml 是否**显式**设过 `defaults.workflow_mode`
|
|
2125
|
+
*/
|
|
2126
|
+
export function shouldPromptWorkflowMode(stageHasField, projectHasDefault) {
|
|
2127
|
+
return !stageHasField && !projectHasDefault ? 'prompt' : 'skip';
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* 项目 config.yaml 是否**显式**声明了 `defaults.{key}`(stage-75 T4,D3)。
|
|
2131
|
+
* 必须**裸读 YAML**:`getConfigValue` 经 `ConfigDefaultsSchema` 逐键 `.default()` 填充,
|
|
2132
|
+
* 缺失键亦返回 schema 默认值(实测恒 `fast`),无法区分「设过」与「未设」。
|
|
2133
|
+
* 文件缺失 / 解析失败 / 无 defaults 块 / 非对象 → false(视为未显式设置)。
|
|
2134
|
+
* @param projectPath 项目根路径
|
|
2135
|
+
* @param key defaults 下的配置键(如 workflow_mode)
|
|
2136
|
+
*/
|
|
2137
|
+
function hasExplicitProjectDefault(projectPath, key) {
|
|
2138
|
+
try {
|
|
2139
|
+
const configPath = resolve(projectPath, '.openfeel', 'config.yaml');
|
|
2140
|
+
if (!existsSync(configPath)) {
|
|
2141
|
+
return false;
|
|
2142
|
+
}
|
|
2143
|
+
const raw = parseYaml(readFileSync(configPath, 'utf-8'));
|
|
2144
|
+
const defaults = raw?.defaults;
|
|
2145
|
+
if (!defaults || typeof defaults !== 'object') {
|
|
2146
|
+
return false;
|
|
2147
|
+
}
|
|
2148
|
+
return Object.prototype.hasOwnProperty.call(defaults, key);
|
|
2149
|
+
}
|
|
2150
|
+
catch {
|
|
2151
|
+
// 错误路径:YAML 解析失败 → 视为未显式设置(不阻断向导)
|
|
2152
|
+
return false;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
1723
2155
|
/** 创建 FlowManager 实例(使用当前工作目录) */
|
|
1724
2156
|
function createManager() {
|
|
1725
2157
|
return new FlowManager(process.cwd());
|