openfeel 1.2.1 → 1.2.3
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 +23 -1
- package/README.md +23 -1
- package/README.zh-CN.md +23 -1
- package/dist/cli/index.d.ts +17 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +96 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/project-option.d.ts +40 -0
- package/dist/cli/project-option.d.ts.map +1 -0
- package/dist/cli/project-option.js +140 -0
- package/dist/cli/project-option.js.map +1 -0
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/archive.js +32 -2
- package/dist/commands/archive.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +65 -7
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/flow.d.ts.map +1 -1
- package/dist/commands/flow.js +394 -114
- package/dist/commands/flow.js.map +1 -1
- package/dist/commands/instructions.d.ts.map +1 -1
- package/dist/commands/instructions.js +13 -3
- package/dist/commands/instructions.js.map +1 -1
- package/dist/commands/knowledge.d.ts.map +1 -1
- package/dist/commands/knowledge.js +113 -17
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +24 -6
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/project.d.ts +66 -0
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +122 -64
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/req.d.ts.map +1 -1
- package/dist/commands/req.js +23 -11
- package/dist/commands/req.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +11 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/shared/errors.d.ts +55 -4
- package/dist/commands/shared/errors.d.ts.map +1 -1
- package/dist/commands/shared/errors.js +108 -10
- package/dist/commands/shared/errors.js.map +1 -1
- package/dist/commands/shared/json-output.d.ts +60 -0
- package/dist/commands/shared/json-output.d.ts.map +1 -0
- package/dist/commands/shared/json-output.js +152 -0
- package/dist/commands/shared/json-output.js.map +1 -0
- package/dist/commands/stage.d.ts.map +1 -1
- package/dist/commands/stage.js +62 -6
- package/dist/commands/stage.js.map +1 -1
- package/dist/commands/view.d.ts.map +1 -1
- package/dist/commands/view.js +27 -0
- package/dist/commands/view.js.map +1 -1
- package/dist/core/config.js +2 -2
- package/dist/core/errors.d.ts +100 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +58 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/flow-manager.d.ts +13 -0
- package/dist/core/flow-manager.d.ts.map +1 -1
- package/dist/core/flow-manager.js +22 -0
- package/dist/core/flow-manager.js.map +1 -1
- package/dist/core/fs/file-lock.d.ts +9 -0
- package/dist/core/fs/file-lock.d.ts.map +1 -1
- package/dist/core/fs/file-lock.js +27 -1
- package/dist/core/fs/file-lock.js.map +1 -1
- package/dist/core/i18n-data/en.d.ts.map +1 -1
- package/dist/core/i18n-data/en.js +42 -3
- package/dist/core/i18n-data/en.js.map +1 -1
- package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
- package/dist/core/i18n-data/zh-CN.js +42 -3
- package/dist/core/i18n-data/zh-CN.js.map +1 -1
- package/dist/core/pipeline-schema.d.ts +7 -0
- package/dist/core/pipeline-schema.d.ts.map +1 -1
- package/dist/core/pipeline-schema.js +7 -0
- package/dist/core/pipeline-schema.js.map +1 -1
- package/dist/core/req/aggregate.d.ts +19 -1
- package/dist/core/req/aggregate.d.ts.map +1 -1
- package/dist/core/req/aggregate.js +18 -5
- package/dist/core/req/aggregate.js.map +1 -1
- package/dist/core/template-loader.d.ts.map +1 -1
- package/dist/core/template-loader.js +33 -9
- package/dist/core/template-loader.js.map +1 -1
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +29 -5
- package/dist/core/update.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/flow.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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, 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';
|
|
4
|
+
import { FlowManager, isFlowConcurrentError, normalizeAgentName, detectFillState, MODE_DERIVED_TIER, listStageBlockingOpenReviews, PHASE_PATH_MAX_DEPTH } from '../core/flow-manager.js';
|
|
5
|
+
import { PipelinePhaseSchema, PIPELINE_PHASES, REVIEW_TIERS, TIER_PRUNED_PHASES, DEFAULT_EFFECTIVE_REVIEW_TIER } from '../core/pipeline-schema.js';
|
|
6
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
9
|
import { MetricsStore, PHASE_AGENT_MAP } from '../core/metrics.js';
|
|
10
10
|
import { runtimeLog } from '../core/runtime-log.js';
|
|
11
11
|
import { addReviewEntry } from '../core/view/entry.js';
|
|
12
|
-
import { handleAddStageError, handleConcurrentConflict } from './shared/errors.js';
|
|
12
|
+
import { failCli, failInternal, handleAddStageError, handleConcurrentConflict } from './shared/errors.js';
|
|
13
|
+
import { withSchemaVersion, isJsonMode, emitJson } from './shared/json-output.js'; // JSON 契约助手(stage-95 op-001/op-002)
|
|
13
14
|
import { t, getCliLang } from '../core/i18n.js';
|
|
14
15
|
import { ARCHIVE_SCHEMA_VERSION } from '../core/flow-archive/archive-file.js';
|
|
15
16
|
import { listArchivableCandidates, listBlockingOpenRevsForVersion, splitForArchive } from '../core/flow-archive/grouping.js';
|
|
@@ -142,6 +143,109 @@ export function registerFlowCommand(program) {
|
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
});
|
|
146
|
+
// flow next — 当前阶段调度决策(只读;P0-1)
|
|
147
|
+
flow
|
|
148
|
+
.command('next')
|
|
149
|
+
.description('显示当前阶段的调度决策(负责 Agent / 合法下一相位 / 建议命令 / 守卫 / 阻塞项;只读)')
|
|
150
|
+
.option('--json', 'Output as JSON (machine-readable, single JSON document)')
|
|
151
|
+
.action(() => {
|
|
152
|
+
const lang = getCliLang(process.cwd());
|
|
153
|
+
const mgr = createManager();
|
|
154
|
+
if (!mgr.isLoaded()) {
|
|
155
|
+
const message = t('common.errorNoInit', lang);
|
|
156
|
+
failCli({ code: 'NO_INIT', message, detail: null, stderrLines: [message] });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const data = mgr.getData();
|
|
160
|
+
const rawStage = data.pipeline.current.stage || '';
|
|
161
|
+
// 双键回退(与 core resolveCurrentPhase / advanceStagePhase 同范式):归一化键优先,其次原始键
|
|
162
|
+
const normalized = rawStage ? (normalizeStageId(rawStage) ?? rawStage) : '';
|
|
163
|
+
const activeKey = rawStage
|
|
164
|
+
? (data.stages[normalized] ? normalized : (data.stages[rawStage] ? rawStage : null))
|
|
165
|
+
: null;
|
|
166
|
+
let phase = null;
|
|
167
|
+
let phaseLabel = null;
|
|
168
|
+
let responsibleAgent = 'feel'; // 无活跃阶段显式约定
|
|
169
|
+
let workflowMode = null;
|
|
170
|
+
let reviewTier = null;
|
|
171
|
+
let effectiveReviewTier = null;
|
|
172
|
+
let legalNextPhases = [];
|
|
173
|
+
let suggestedCommand = null;
|
|
174
|
+
const guards = [];
|
|
175
|
+
let blockingReviews = [];
|
|
176
|
+
if (activeKey) {
|
|
177
|
+
const currentPhase = data.stages[activeKey].phase;
|
|
178
|
+
phase = currentPhase;
|
|
179
|
+
phaseLabel = mgr.getPhaseLabels(lang)[currentPhase] ?? currentPhase;
|
|
180
|
+
responsibleAgent = mgr.getResponsibleAgent(currentPhase);
|
|
181
|
+
workflowMode = mgr.resolveStageWorkflowMode(activeKey).value;
|
|
182
|
+
reviewTier = MODE_DERIVED_TIER[workflowMode];
|
|
183
|
+
effectiveReviewTier = DEFAULT_EFFECTIVE_REVIEW_TIER;
|
|
184
|
+
legalNextPhases = mgr.getAvailablePhases(activeKey);
|
|
185
|
+
suggestedCommand = legalNextPhases.length > 0
|
|
186
|
+
? `openfeel flow advance --stage ${activeKey} --to ${legalNextPhases[0]}`
|
|
187
|
+
: null;
|
|
188
|
+
blockingReviews = listStageBlockingOpenReviews(data.reviews, activeKey).map((r) => ({
|
|
189
|
+
id: r.id, title: r.title, priority: r.priority, op: r.op,
|
|
190
|
+
}));
|
|
191
|
+
// 守卫顺序契约固定(§12.1):auto-advance → blocking → 无目标 → 档位差
|
|
192
|
+
if (mgr.resolveEffectiveConfig()['auto_advance']?.value === 'disabled') {
|
|
193
|
+
guards.push(t('flow.next.guardAutoAdvanceDisabled', lang));
|
|
194
|
+
}
|
|
195
|
+
if (blockingReviews.length > 0) {
|
|
196
|
+
guards.push(t('flow.next.guardBlockingReviewsTmpl', lang, { n: String(blockingReviews.length) }));
|
|
197
|
+
}
|
|
198
|
+
if (legalNextPhases.length === 0) {
|
|
199
|
+
guards.push(t('flow.next.guardNoLegalTargets', lang));
|
|
200
|
+
}
|
|
201
|
+
if (reviewTier !== effectiveReviewTier) {
|
|
202
|
+
guards.push(t('flow.next.guardTierDiffTmpl', lang, { display: reviewTier, effective: effectiveReviewTier }));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
guards.push(t('flow.next.guardNoActiveStage', lang));
|
|
207
|
+
}
|
|
208
|
+
const doc = withSchemaVersion({
|
|
209
|
+
stage: activeKey ?? (rawStage || null),
|
|
210
|
+
phase,
|
|
211
|
+
phaseLabel,
|
|
212
|
+
responsibleAgent,
|
|
213
|
+
workflowMode,
|
|
214
|
+
reviewTier,
|
|
215
|
+
effectiveReviewTier,
|
|
216
|
+
legalNextPhases,
|
|
217
|
+
suggestedCommand,
|
|
218
|
+
guards,
|
|
219
|
+
blockingReviews,
|
|
220
|
+
});
|
|
221
|
+
// JSON 模式单点(stage-95 不变量 2):命令层只读 isJsonMode()
|
|
222
|
+
if (isJsonMode()) {
|
|
223
|
+
emitJson(doc);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// 人类输出(逐行标签;空值统一 t('common.none', lang))
|
|
227
|
+
console.log(t('flow.next.title', lang));
|
|
228
|
+
console.log(` ${t('flow.next.stageLabel', lang)}: ${doc.stage ?? t('common.none', lang)}`);
|
|
229
|
+
console.log(` ${t('flow.next.phaseLabel', lang)}: ${doc.phase ?? t('common.none', lang)}${doc.phaseLabel ? ` (${doc.phaseLabel})` : ''}`);
|
|
230
|
+
console.log(` ${t('flow.next.responsibleAgentLabel', lang)}: ${doc.responsibleAgent}`);
|
|
231
|
+
console.log(` ${t('flow.next.workflowModeLabel', lang)}: ${doc.workflowMode ?? t('common.none', lang)}`);
|
|
232
|
+
console.log(` ${t('flow.next.displayTierLabel', lang)}: ${doc.reviewTier ?? t('common.none', lang)}`);
|
|
233
|
+
console.log(` ${t('flow.next.effectiveTierLabel', lang)}: ${doc.effectiveReviewTier ?? t('common.none', lang)}`);
|
|
234
|
+
console.log(` ${t('flow.next.legalNextPhasesLabel', lang)}: ${doc.legalNextPhases.length > 0 ? `[${doc.legalNextPhases.join(', ')}]` : t('common.none', lang)}`);
|
|
235
|
+
console.log(` ${t('flow.next.suggestedCommandLabel', lang)}: ${doc.suggestedCommand ?? t('common.none', lang)}`);
|
|
236
|
+
if (doc.guards.length > 0) {
|
|
237
|
+
console.log(` ${t('flow.next.guardsLabel', lang)}:`);
|
|
238
|
+
for (const g of doc.guards) {
|
|
239
|
+
console.log(` - ${g}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (doc.blockingReviews.length > 0) {
|
|
243
|
+
console.log(` ${t('flow.next.blockingReviewsLabel', lang)}:`);
|
|
244
|
+
for (const r of doc.blockingReviews) {
|
|
245
|
+
console.log(` - ${t('flow.next.blockingReviewItemTmpl', lang, { id: r.id, title: r.title, priority: r.priority, op: r.op })}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
145
249
|
// flow overview — 全状态可视化(openfeel flow overview 的后端实现)
|
|
146
250
|
flow
|
|
147
251
|
.command('overview')
|
|
@@ -360,8 +464,13 @@ export function registerFlowCommand(program) {
|
|
|
360
464
|
if (options.mode !== undefined) {
|
|
361
465
|
const allowed = getConfigFieldLegalValues('workflow_mode') ?? [];
|
|
362
466
|
if (!allowed.includes(options.mode)) {
|
|
363
|
-
|
|
364
|
-
|
|
467
|
+
const message = t('stage.set.invalidValueTmpl', lang, { field: '工作流模式', value: options.mode, allowed: allowed.join(', ') });
|
|
468
|
+
failCli({
|
|
469
|
+
code: 'INVALID_ARGUMENT',
|
|
470
|
+
message,
|
|
471
|
+
detail: { field: 'workflowMode', value: options.mode, allowed },
|
|
472
|
+
stderrLines: [message],
|
|
473
|
+
});
|
|
365
474
|
return;
|
|
366
475
|
}
|
|
367
476
|
explicitMode = options.mode;
|
|
@@ -371,17 +480,15 @@ export function registerFlowCommand(program) {
|
|
|
371
480
|
if (options.stage !== undefined) {
|
|
372
481
|
const v = validateStageId(options.stage);
|
|
373
482
|
if (!v.ok) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
process.exit(1);
|
|
483
|
+
const stderrLines = stageIdErrorLines(lang, projectPath, options.stage);
|
|
484
|
+
failCli({ code: 'INVALID_ARGUMENT', message: stderrLines[0], detail: { stage: options.stage }, stderrLines });
|
|
377
485
|
return;
|
|
378
486
|
}
|
|
379
487
|
stageArg = normalizeStageId(options.stage) ?? options.stage;
|
|
380
488
|
const known = new Set(Object.keys(mgr.isLoaded() ? mgr.getData().stages : {}).map((k) => normalizeStageId(k) ?? k));
|
|
381
489
|
if (!known.has(stageArg) && !findStageStatusPath(projectPath, stageArg)) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
process.exit(1);
|
|
490
|
+
const stderrLines = stageIdErrorLines(lang, projectPath, options.stage);
|
|
491
|
+
failCli({ code: 'STAGE_NOT_FOUND', message: stderrLines[0], detail: { stage: options.stage }, stderrLines });
|
|
385
492
|
return;
|
|
386
493
|
}
|
|
387
494
|
}
|
|
@@ -401,7 +508,9 @@ export function registerFlowCommand(program) {
|
|
|
401
508
|
// advanceAccepted = 内置 15 phase(flow advance 的推进白名单),与 phases(运行时存在视图,可含自定义)显式区分
|
|
402
509
|
// 既有三字段(phases/transitions/advanceAccepted)+ transitionsDiff 逐字保留;schemaVersion 与 workflowMode 追加于末位(B1-6 / stage-75 op-002)
|
|
403
510
|
// reviewTier 再追加于末位(stage-92 op-001;既有 6 键逐字保留、顺序不变)
|
|
404
|
-
|
|
511
|
+
// stage-96 op-002:末位再追加 displayReviewTier(与 reviewTier 同值显式名)与 effectiveReviewTier(推进缺省档位,恒 full)
|
|
512
|
+
const displayTier = MODE_DERIVED_TIER[derivedMode];
|
|
513
|
+
console.log(JSON.stringify({ schemaVersion: 1, phases, transitions, advanceAccepted: [...PIPELINE_PHASES], transitionsDiff, workflowMode: derivedMode, reviewTier: displayTier, displayReviewTier: displayTier, effectiveReviewTier: DEFAULT_EFFECTIVE_REVIEW_TIER }, null, 2));
|
|
405
514
|
return;
|
|
406
515
|
}
|
|
407
516
|
console.log(t('flow.phases.title', lang));
|
|
@@ -456,18 +565,16 @@ export function registerFlowCommand(program) {
|
|
|
456
565
|
if (options.stage !== undefined) {
|
|
457
566
|
const v = validateStageId(options.stage);
|
|
458
567
|
if (!v.ok) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
process.exit(1);
|
|
568
|
+
const stderrLines = stageIdErrorLines(lang, projectPath, options.stage);
|
|
569
|
+
failCli({ code: 'INVALID_ARGUMENT', message: stderrLines[0], detail: { stage: options.stage }, stderrLines });
|
|
462
570
|
return;
|
|
463
571
|
}
|
|
464
572
|
key = normalizeStageId(options.stage) ?? options.stage;
|
|
465
573
|
// 存在性:已注册(flow.json)或有 status.md;二者皆无 → exit 1(防 typo 静默落 config 层)
|
|
466
574
|
const known = new Set(Object.keys(mgr.isLoaded() ? mgr.getData().stages : {}).map((k) => normalizeStageId(k) ?? k));
|
|
467
575
|
if (!known.has(key) && !findStageStatusPath(projectPath, key)) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
process.exit(1);
|
|
576
|
+
const stderrLines = stageIdErrorLines(lang, projectPath, options.stage);
|
|
577
|
+
failCli({ code: 'STAGE_NOT_FOUND', message: stderrLines[0], detail: { stage: options.stage }, stderrLines });
|
|
471
578
|
return;
|
|
472
579
|
}
|
|
473
580
|
}
|
|
@@ -599,9 +706,10 @@ export function registerFlowCommand(program) {
|
|
|
599
706
|
console.log(t('flow.stage.remove.okTmpl', lang, { stage: stageArg }));
|
|
600
707
|
}
|
|
601
708
|
catch (err) {
|
|
602
|
-
// 并发写冲突:统一单点(i18n 文案 + 退出码 2
|
|
709
|
+
// 并发写冲突:统一单点(i18n 文案 + 退出码 2);JSON 模式置码返回 → 显式 return 防落入通用分支
|
|
603
710
|
if (isFlowConcurrentError(err)) {
|
|
604
711
|
handleConcurrentConflict(err, lang);
|
|
712
|
+
return;
|
|
605
713
|
}
|
|
606
714
|
const msg = err instanceof Error ? err.message : String(err);
|
|
607
715
|
console.error(t('common.errorTmpl', lang, { msg }));
|
|
@@ -658,8 +766,10 @@ export function registerFlowCommand(program) {
|
|
|
658
766
|
mgr.setStageDeps(stageId, deps);
|
|
659
767
|
}
|
|
660
768
|
catch (err) {
|
|
769
|
+
// 并发写冲突:统一单点(i18n 文案 + 退出码 2);JSON 模式置码返回 → 显式 return 防落入通用分支
|
|
661
770
|
if (isFlowConcurrentError(err)) {
|
|
662
771
|
handleConcurrentConflict(err, lang);
|
|
772
|
+
return;
|
|
663
773
|
}
|
|
664
774
|
const msg = err instanceof Error ? err.message : String(err);
|
|
665
775
|
console.error(t('common.errorTmpl', lang, { msg }));
|
|
@@ -710,9 +820,10 @@ export function registerFlowCommand(program) {
|
|
|
710
820
|
}));
|
|
711
821
|
}
|
|
712
822
|
catch (err) {
|
|
713
|
-
// 并发写冲突:统一单点(i18n 文案 + 退出码 2
|
|
823
|
+
// 并发写冲突:统一单点(i18n 文案 + 退出码 2);JSON 模式置码返回 → 显式 return 防落入通用分支
|
|
714
824
|
if (isFlowConcurrentError(err)) {
|
|
715
825
|
handleConcurrentConflict(err, lang);
|
|
826
|
+
return;
|
|
716
827
|
}
|
|
717
828
|
const msg = err instanceof Error ? err.message : String(err);
|
|
718
829
|
console.error(t('common.errorTmpl', lang, { msg }));
|
|
@@ -728,32 +839,49 @@ export function registerFlowCommand(program) {
|
|
|
728
839
|
.option('--stage <id>', '阶段 ID(如 stage-03),必须指定')
|
|
729
840
|
.option('--force', '强制执行(跳过非法 phase 校验和阶段跳跃检查,但不可绕过 REV 阻塞检查)')
|
|
730
841
|
.option('--dry-run', '仅验证不执行修改(预览输出)。与 --force 组合时跳过校验但仍不执行修改')
|
|
731
|
-
.option('--review-tier <tier>', '审查档位(full / lean / skip
|
|
842
|
+
.option('--review-tier <tier>', '审查档位(full / lean / skip;缺省=full(零裁剪);仅显式声明时裁剪审查关,不可绕过 REV 阻塞检查)')
|
|
843
|
+
.option('--json', '以 JSON 输出错误封装(仅失败时;成功输出保持人类文本)')
|
|
732
844
|
.option('--quiet', '静默非错误输出(成功确认与 Git 警告均不打印)')
|
|
733
845
|
.action((options) => {
|
|
734
846
|
const lang = getCliLang(process.cwd());
|
|
847
|
+
// JSON 模式(--json 为错误封装开关)下,失败前的人类进展行改走 stderr,保证 stdout 仅一个 envelope;
|
|
848
|
+
// 成功路径的人类输出保持 console.log(advance 成功不 JSON 化,契约 §7/§8)。
|
|
849
|
+
const info = (line) => {
|
|
850
|
+
if (isJsonMode()) {
|
|
851
|
+
console.error(line);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
console.log(line);
|
|
855
|
+
};
|
|
735
856
|
// 自定义 --stage 必选校验(提供中文错误提示)
|
|
736
857
|
if (!options.stage) {
|
|
737
|
-
|
|
738
|
-
|
|
858
|
+
const message = t('flow.advance.errorNoStage', lang);
|
|
859
|
+
failCli({ code: 'INVALID_ARGUMENT', message, detail: { required: '--stage' }, stderrLines: [message] });
|
|
860
|
+
return;
|
|
739
861
|
}
|
|
740
862
|
// 审查档位值域校验(stage-92 op-001;fail-fast 且**先于任何取值 / 路径解析 / autoRepair 写盘**)
|
|
741
863
|
// 复用 stage.set.invalidValueTmpl(与 flow phases --mode 同款,零新增键);非法 → exit 1 零写盘
|
|
742
864
|
let explicitTier;
|
|
743
865
|
if (options.reviewTier !== undefined) {
|
|
744
866
|
if (!REVIEW_TIERS.includes(options.reviewTier)) {
|
|
745
|
-
|
|
746
|
-
|
|
867
|
+
const message = t('stage.set.invalidValueTmpl', lang, { field: '审查档位', value: options.reviewTier, allowed: REVIEW_TIERS.join(', ') });
|
|
868
|
+
failCli({
|
|
869
|
+
code: 'INVALID_ARGUMENT',
|
|
870
|
+
message,
|
|
871
|
+
detail: { field: 'reviewTier', value: options.reviewTier, allowed: REVIEW_TIERS },
|
|
872
|
+
stderrLines: [message],
|
|
873
|
+
});
|
|
747
874
|
return;
|
|
748
875
|
}
|
|
749
876
|
explicitTier = options.reviewTier;
|
|
750
877
|
}
|
|
751
|
-
// 生效档位:显式声明 ??
|
|
752
|
-
const effTier = explicitTier ??
|
|
878
|
+
// 生效档位:显式声明 ?? DEFAULT_EFFECTIVE_REVIEW_TIER(**不传 = 零裁剪**;派生档仅用于 `flow phases` 展示与提示,不改变推进语义)
|
|
879
|
+
const effTier = explicitTier ?? DEFAULT_EFFECTIVE_REVIEW_TIER;
|
|
753
880
|
const mgr = createManager();
|
|
754
881
|
if (!mgr.isLoaded()) {
|
|
755
|
-
|
|
756
|
-
|
|
882
|
+
const message = t('common.errorNoInit', lang);
|
|
883
|
+
failCli({ code: 'NO_INIT', message, detail: null, stderrLines: [message] });
|
|
884
|
+
return;
|
|
757
885
|
}
|
|
758
886
|
// REV-005:命令层归一化(短名 → 全名),与 plan.ts / flow.ts 其它 stage 解析点同范式
|
|
759
887
|
const stageArg = normalizeStageId(options.stage) ?? options.stage;
|
|
@@ -762,7 +890,8 @@ export function registerFlowCommand(program) {
|
|
|
762
890
|
// B1 修复:dry-run 走预览模式(不写内存/不写盘),仅在非 dry-run 时 save()
|
|
763
891
|
const repairResult = mgr.autoRepairInconsistency(stageArg, { dryRun: options.dryRun });
|
|
764
892
|
if (repairResult.fixed) {
|
|
765
|
-
|
|
893
|
+
// JSON 模式下改走 stderr(失败时 stdout 仅保留一个 envelope)
|
|
894
|
+
info(options.dryRun
|
|
766
895
|
? t('flow.advance.autoRepairPreview', lang) + `: ${repairResult.detail}`
|
|
767
896
|
: t('flow.advance.autoRepaired', lang) + `: ${repairResult.detail}`);
|
|
768
897
|
if (!options.dryRun) {
|
|
@@ -776,23 +905,35 @@ export function registerFlowCommand(program) {
|
|
|
776
905
|
for (const w of warnings) {
|
|
777
906
|
console.warn(`[WARN] ${w}`);
|
|
778
907
|
}
|
|
779
|
-
|
|
908
|
+
// JSON 模式下改走 stderr(失败时 stdout 仅保留一个 envelope)
|
|
909
|
+
info(t('flow.advance.warnAutoCorrect', lang));
|
|
780
910
|
}
|
|
781
911
|
if (!valid) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
912
|
+
const message = t('flow.advance.errorInvalidFormat', lang);
|
|
913
|
+
failCli({
|
|
914
|
+
code: 'INVALID_ARGUMENT',
|
|
915
|
+
message,
|
|
916
|
+
detail: { errors },
|
|
917
|
+
stderrLines: [message, ...errors.map((e) => ' - ' + e)],
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
787
920
|
}
|
|
788
921
|
// 非法 phase 校验(P1):拒绝不在枚举中的目标 phase;--force 可跳过此项,但不可绕过 REV 阻塞检查
|
|
789
922
|
if (!options.force) {
|
|
790
923
|
const phaseResult = PipelinePhaseSchema.safeParse(options.to);
|
|
791
924
|
if (!phaseResult.success) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
925
|
+
const message = t('flow.advance.errorInvalidPhaseTmpl', lang, { phase: options.to });
|
|
926
|
+
failCli({
|
|
927
|
+
code: 'PHASE_TRANSITION_ILLEGAL',
|
|
928
|
+
message,
|
|
929
|
+
detail: { stage: stageArg, to: options.to, allowed: [...PIPELINE_PHASES] },
|
|
930
|
+
stderrLines: [
|
|
931
|
+
message,
|
|
932
|
+
t('flow.advance.labelValidPhases', lang) + `: [${PIPELINE_PHASES.join(', ')}]`,
|
|
933
|
+
t('flow.advance.hintUseForceFuzzy', lang),
|
|
934
|
+
],
|
|
935
|
+
});
|
|
936
|
+
return;
|
|
796
937
|
}
|
|
797
938
|
}
|
|
798
939
|
// 阶段路径解析(B5-1):非 --force 时求「当前 phase → to」的唯一可达路径
|
|
@@ -806,36 +947,47 @@ export function registerFlowCommand(program) {
|
|
|
806
947
|
const data = mgr.getData();
|
|
807
948
|
const currentPhase = data?.stages[stageArg]?.phase ?? t('common.unknown', lang);
|
|
808
949
|
const availableTargets = mgr.getAvailablePhases(stageArg, effTier);
|
|
950
|
+
const stderrLines = [];
|
|
809
951
|
if (pathResult.reason === 'ambiguous') {
|
|
810
|
-
|
|
952
|
+
stderrLines.push(t('flow.advance.ambiguousTmpl', lang));
|
|
811
953
|
}
|
|
812
954
|
else if (pathResult.reason === 'depth-exceeded') {
|
|
813
|
-
|
|
955
|
+
stderrLines.push(t('flow.advance.depthExceededTmpl', lang, { max: String(PHASE_PATH_MAX_DEPTH) }));
|
|
814
956
|
}
|
|
815
957
|
else {
|
|
816
958
|
// general(无流水线)→ 显式错误引导(防御路径:常规写路径不产生 general 阶段态;REV-90-005 / D-90-3)
|
|
817
959
|
if (mgr.resolveStageWorkflowMode(stageArg).value === 'general') {
|
|
818
|
-
|
|
819
|
-
|
|
960
|
+
const message = t('flow.advance.generalNoPipeline', lang, { stage: stageArg });
|
|
961
|
+
failCli({
|
|
962
|
+
code: 'PHASE_PATH_NOT_FOUND',
|
|
963
|
+
message,
|
|
964
|
+
detail: { stage: stageArg, from: currentPhase, to: options.to, reason: pathResult.reason, targets: availableTargets },
|
|
965
|
+
stderrLines: [message],
|
|
966
|
+
});
|
|
820
967
|
return;
|
|
821
968
|
}
|
|
822
|
-
|
|
969
|
+
stderrLines.push(t('flow.advance.noPathTmpl', lang, {
|
|
823
970
|
from: currentPhase,
|
|
824
971
|
to: options.to,
|
|
825
972
|
targets: availableTargets.length > 0 ? `[${availableTargets.join(', ')}]` : t('common.none', lang),
|
|
826
973
|
}));
|
|
827
974
|
}
|
|
828
975
|
// 保留既有跳转诊断(可达目标)——与现状一致
|
|
829
|
-
|
|
830
|
-
|
|
976
|
+
stderrLines.push(t('flow.advance.errorPhaseJumpTmpl', lang, { stage: stageArg, to: options.to }));
|
|
977
|
+
stderrLines.push(t('flow.advance.currentPhaseTmpl', lang, { phase: currentPhase }));
|
|
831
978
|
if (availableTargets.length > 0) {
|
|
832
|
-
|
|
979
|
+
stderrLines.push(t('flow.advance.availableTargets', lang) + `: [${availableTargets.join(', ')}]`);
|
|
833
980
|
}
|
|
834
981
|
else {
|
|
835
|
-
|
|
982
|
+
stderrLines.push(t('flow.advance.noAvailableTargets', lang));
|
|
836
983
|
}
|
|
837
|
-
|
|
838
|
-
|
|
984
|
+
stderrLines.push(t('flow.advance.hintUseForce', lang));
|
|
985
|
+
failCli({
|
|
986
|
+
code: 'PHASE_PATH_NOT_FOUND',
|
|
987
|
+
message: stderrLines[0],
|
|
988
|
+
detail: { stage: stageArg, from: currentPhase, to: options.to, reason: pathResult.reason, targets: availableTargets },
|
|
989
|
+
stderrLines,
|
|
990
|
+
});
|
|
839
991
|
return;
|
|
840
992
|
}
|
|
841
993
|
if (pathResult.reason === 'already-at-target') {
|
|
@@ -870,9 +1022,13 @@ export function registerFlowCommand(program) {
|
|
|
870
1022
|
if (options.force) {
|
|
871
1023
|
console.warn(t('flow.advance.forceRevRefused', lang));
|
|
872
1024
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1025
|
+
const message = t('flow.advance.blockingRevRefused', lang);
|
|
1026
|
+
failCli({
|
|
1027
|
+
code: 'BLOCKING_REVIEWS',
|
|
1028
|
+
message,
|
|
1029
|
+
detail: { stage: stageArg, to: 'done', reviews: blockingOpen.map((r) => r.id) },
|
|
1030
|
+
stderrLines: [message, t('flow.advance.blockingRevHint', lang)],
|
|
1031
|
+
});
|
|
876
1032
|
return;
|
|
877
1033
|
}
|
|
878
1034
|
}
|
|
@@ -919,14 +1075,21 @@ export function registerFlowCommand(program) {
|
|
|
919
1075
|
archived = mgr.advanceStagePhase(stageArg, next, 'cli', explicitTier ? { reviewTier: explicitTier } : undefined);
|
|
920
1076
|
}
|
|
921
1077
|
catch (err) {
|
|
1078
|
+
const stderrLines = [];
|
|
922
1079
|
if (totalSteps > 1) {
|
|
923
1080
|
const remaining = phasePath.slice(i).join(t('flow.advance.pathArrow', lang));
|
|
924
1081
|
const doneTo = i > 0 ? phasePath[i - 1] : t('common.none', lang);
|
|
925
|
-
|
|
1082
|
+
stderrLines.push(t('flow.advance.partialFailTmpl', lang, { done: doneTo, failed: next, remaining }));
|
|
926
1083
|
}
|
|
927
1084
|
const msg = err instanceof Error ? err.message : String(err);
|
|
928
|
-
|
|
929
|
-
|
|
1085
|
+
const message = t('common.errorTmpl', lang, { msg });
|
|
1086
|
+
stderrLines.push(message);
|
|
1087
|
+
// 多步循环中的意外异常归内部错误(D-95-11:原 exit 1 → exit 3,登记为分类补全)
|
|
1088
|
+
failInternal(err, {
|
|
1089
|
+
message,
|
|
1090
|
+
stderrLines,
|
|
1091
|
+
detail: { stage: stageArg, failed: next, remaining: phasePath.slice(i) },
|
|
1092
|
+
});
|
|
930
1093
|
return;
|
|
931
1094
|
}
|
|
932
1095
|
archivedAny = archivedAny || archived;
|
|
@@ -965,11 +1128,16 @@ export function registerFlowCommand(program) {
|
|
|
965
1128
|
const blockingOpen = assertNoBlockingOpenRev(mgr, stageArg, lang);
|
|
966
1129
|
if (blockingOpen.length > 0) {
|
|
967
1130
|
const remainingPath = phasePath.slice(i + 1);
|
|
968
|
-
|
|
1131
|
+
const message = t('flow.advance.blockedByRevTmpl', lang, {
|
|
969
1132
|
revs: blockingOpen.map((r) => r.id).join(', '),
|
|
970
1133
|
remaining: remainingPath.length > 0 ? remainingPath.join(t('flow.advance.pathArrow', lang)) : t('common.none', lang),
|
|
971
|
-
})
|
|
972
|
-
|
|
1134
|
+
});
|
|
1135
|
+
failCli({
|
|
1136
|
+
code: 'BLOCKING_REVIEWS',
|
|
1137
|
+
message,
|
|
1138
|
+
detail: { stage: stageArg, to: 'done', reviews: blockingOpen.map((r) => r.id) },
|
|
1139
|
+
stderrLines: [message],
|
|
1140
|
+
});
|
|
973
1141
|
return;
|
|
974
1142
|
}
|
|
975
1143
|
}
|
|
@@ -1102,14 +1270,14 @@ export function registerFlowCommand(program) {
|
|
|
1102
1270
|
}
|
|
1103
1271
|
}
|
|
1104
1272
|
if (options.json) {
|
|
1105
|
-
// 复用 op-001 口径:顶层对象 + schemaVersion:1
|
|
1273
|
+
// 复用 op-001 口径:顶层对象 + schemaVersion:1;warning 恒在(非 empty 为 null)
|
|
1106
1274
|
const ops = items.map((it) => ({
|
|
1107
1275
|
stage: it.stage,
|
|
1108
1276
|
opId: it.opId,
|
|
1109
1277
|
title: it.title,
|
|
1110
1278
|
state: it.state,
|
|
1111
1279
|
fill: it.fill,
|
|
1112
|
-
|
|
1280
|
+
warning: it.fill === 'empty' ? t('flow.ops.emptyWarningTmpl', lang, { opId: it.opId }) : null,
|
|
1113
1281
|
}));
|
|
1114
1282
|
console.log(JSON.stringify({ schemaVersion: 1, ops }, null, 2));
|
|
1115
1283
|
return;
|
|
@@ -1138,14 +1306,29 @@ export function registerFlowCommand(program) {
|
|
|
1138
1306
|
.command('log')
|
|
1139
1307
|
.description('显示最近操作日志')
|
|
1140
1308
|
.option('--last <n>', '显示最近 n 条(默认 10)', '10')
|
|
1309
|
+
.option('--json', t('help.flow.log.json', getCliLang(process.cwd())))
|
|
1141
1310
|
.action((options) => {
|
|
1142
1311
|
const lang = getCliLang(process.cwd());
|
|
1143
1312
|
const mgr = createManager();
|
|
1313
|
+
const n = Math.max(1, parseInt(options.last, 10) || 10);
|
|
1314
|
+
// JSON 分支须为首行纯 JSON 单文档;空日志与未初始化同形(entries: [])
|
|
1315
|
+
if (options.json) {
|
|
1316
|
+
const entries = mgr.isLoaded() && mgr.getData() ? mgr.getData().log.slice(-n) : [];
|
|
1317
|
+
emitJson(withSchemaVersion({
|
|
1318
|
+
count: entries.length,
|
|
1319
|
+
entries: entries.map((e) => ({
|
|
1320
|
+
time: e.time,
|
|
1321
|
+
agent: normalizeAgentName(e.agent),
|
|
1322
|
+
action: e.action,
|
|
1323
|
+
detail: e.detail,
|
|
1324
|
+
})),
|
|
1325
|
+
}));
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1144
1328
|
if (!mgr.isLoaded()) {
|
|
1145
1329
|
console.log(t('flow.log.noInit', lang));
|
|
1146
1330
|
return;
|
|
1147
1331
|
}
|
|
1148
|
-
const n = Math.max(1, parseInt(options.last, 10) || 10);
|
|
1149
1332
|
const data = mgr.getData();
|
|
1150
1333
|
if (!data || data.log.length === 0) {
|
|
1151
1334
|
console.log(t('flow.log.noLogs', lang));
|
|
@@ -1513,10 +1696,20 @@ export function registerFlowCommand(program) {
|
|
|
1513
1696
|
.command('list')
|
|
1514
1697
|
.description('列出所有(或指定阶段的)Checkpoint 快照')
|
|
1515
1698
|
.argument('[stage]', '阶段 ID(可选),如 v5.3-stage-01')
|
|
1516
|
-
.
|
|
1699
|
+
.option('--json', t('help.flow.checkpoint.list.json', getCliLang(process.cwd())))
|
|
1700
|
+
.action((stage, options) => {
|
|
1517
1701
|
const lang = getCliLang(process.cwd());
|
|
1518
1702
|
const mgr = createManager();
|
|
1519
1703
|
const snapshots = mgr.listCheckpoints(stage);
|
|
1704
|
+
// JSON 分支:未初始化与空集合同形(checkpoints: [])
|
|
1705
|
+
if (options.json) {
|
|
1706
|
+
emitJson(withSchemaVersion({
|
|
1707
|
+
stage: stage ?? null,
|
|
1708
|
+
checkpoints: snapshots,
|
|
1709
|
+
count: snapshots.length,
|
|
1710
|
+
}));
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1520
1713
|
if (snapshots.length === 0) {
|
|
1521
1714
|
console.log(stage
|
|
1522
1715
|
? t('flow.checkpoint.noSnapshotsStageTmpl', lang, { stage })
|
|
@@ -1600,18 +1793,19 @@ export function registerFlowCommand(program) {
|
|
|
1600
1793
|
const projectPath = process.cwd();
|
|
1601
1794
|
const lang = getCliLang(projectPath);
|
|
1602
1795
|
const mgr = createManager();
|
|
1603
|
-
// --json
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1796
|
+
// --json 失败出口统一(legacy 形态):纯 JSON 单文档、不混人类文案;
|
|
1797
|
+
// `errorKind` 恒为末尾键,`error` 恒 string、既有键序零改动(REV-95-002 / D-95-09)
|
|
1798
|
+
const failJson = (payload, errorKind = 'refusal', exitCode = 1) => {
|
|
1799
|
+
console.log(JSON.stringify({ schemaVersion: 1, ...payload, errorKind }, null, 2));
|
|
1800
|
+
process.exitCode = exitCode;
|
|
1607
1801
|
};
|
|
1608
1802
|
if (!mgr.isLoaded()) {
|
|
1609
1803
|
if (options.json) {
|
|
1610
1804
|
failJson({ error: 'no-init' });
|
|
1611
1805
|
return;
|
|
1612
1806
|
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1807
|
+
const message = t('common.errorNoInit', lang);
|
|
1808
|
+
failCli({ code: 'NO_INIT', message, detail: null, stderrLines: [message] });
|
|
1615
1809
|
return;
|
|
1616
1810
|
}
|
|
1617
1811
|
// 版本归一(REV-79-004):去 v 再加 v;非法 → exit 1 零写盘
|
|
@@ -1621,8 +1815,8 @@ export function registerFlowCommand(program) {
|
|
|
1621
1815
|
failJson({ error: 'invalid-version', input: versionInput });
|
|
1622
1816
|
return;
|
|
1623
1817
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1818
|
+
const message = t('flow.archive.invalidVersionTmpl', lang, { input: versionInput });
|
|
1819
|
+
failCli({ code: 'INVALID_ARGUMENT', message, detail: { input: versionInput }, stderrLines: [message] });
|
|
1626
1820
|
return;
|
|
1627
1821
|
}
|
|
1628
1822
|
const data = mgr.getData();
|
|
@@ -1633,8 +1827,8 @@ export function registerFlowCommand(program) {
|
|
|
1633
1827
|
failJson({ error: 'version-not-active', version });
|
|
1634
1828
|
return;
|
|
1635
1829
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1830
|
+
const message = t('flow.archive.versionNotActiveTmpl', lang, { version });
|
|
1831
|
+
failCli({ code: 'INVALID_ARGUMENT', message, detail: { version }, stderrLines: [message] });
|
|
1638
1832
|
return;
|
|
1639
1833
|
}
|
|
1640
1834
|
// 判据门:全 done(candidate.allDone 空集守卫)+ 无 blocking open REV(全仓唯一谓词,REV-79-002)
|
|
@@ -1642,23 +1836,38 @@ export function registerFlowCommand(program) {
|
|
|
1642
1836
|
const blockingOpen = listBlockingOpenRevsForVersion(data, version);
|
|
1643
1837
|
if (!candidate.allDone || blockingOpen.length > 0) {
|
|
1644
1838
|
if (options.json) {
|
|
1645
|
-
|
|
1839
|
+
// 判据明细(D-97-03-04):legacy 五键序保留,三新键插在 blockingOpen 后、errorKind 前
|
|
1840
|
+
failJson({
|
|
1841
|
+
error: 'gate-rejected',
|
|
1842
|
+
version,
|
|
1843
|
+
notDone,
|
|
1844
|
+
blockingOpen: blockingOpen.map((r) => r.id),
|
|
1845
|
+
eligible: false,
|
|
1846
|
+
unfinishedStages: notDone.map((id) => ({ id, phase: data.stages[id]?.phase ?? null })),
|
|
1847
|
+
blockingReviews: blockingOpen.map((r) => ({ id: r.id, title: r.title, priority: r.priority })),
|
|
1848
|
+
});
|
|
1646
1849
|
return;
|
|
1647
1850
|
}
|
|
1851
|
+
const stderrLines = [];
|
|
1648
1852
|
if (notDone.length > 0) {
|
|
1649
|
-
|
|
1853
|
+
stderrLines.push(t('flow.archive.gateNotDoneTitleTmpl', lang, { version, n: String(notDone.length) }));
|
|
1650
1854
|
for (const id of notDone) {
|
|
1651
|
-
|
|
1855
|
+
stderrLines.push(t('flow.archive.gateItemTmpl', lang, { item: id + ' [' + (data.stages[id]?.phase ?? '') + ']' }));
|
|
1652
1856
|
}
|
|
1653
1857
|
}
|
|
1654
1858
|
if (blockingOpen.length > 0) {
|
|
1655
|
-
|
|
1859
|
+
stderrLines.push(t('flow.archive.gateBlockingRevTitleTmpl', lang, { version, n: String(blockingOpen.length) }));
|
|
1656
1860
|
for (const r of blockingOpen) {
|
|
1657
|
-
|
|
1861
|
+
stderrLines.push(t('flow.archive.gateItemTmpl', lang, { item: r.id + ': ' + r.title + ' (priority=' + r.priority + ')' }));
|
|
1658
1862
|
}
|
|
1659
1863
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1864
|
+
stderrLines.push(t('flow.archive.gateRejected', lang));
|
|
1865
|
+
failCli({
|
|
1866
|
+
code: 'ARCHIVE_GATE_REJECTED',
|
|
1867
|
+
message: stderrLines[stderrLines.length - 1],
|
|
1868
|
+
detail: { version, notDone, blockingOpen: blockingOpen.map((r) => r.id) },
|
|
1869
|
+
stderrLines,
|
|
1870
|
+
});
|
|
1662
1871
|
return;
|
|
1663
1872
|
}
|
|
1664
1873
|
// 切分(纯函数;禁命令层第二套解析/切分)
|
|
@@ -1678,7 +1887,11 @@ export function registerFlowCommand(program) {
|
|
|
1678
1887
|
};
|
|
1679
1888
|
const preview = applyArchivePipelineDisposition(previewData, version);
|
|
1680
1889
|
if (options.json) {
|
|
1681
|
-
|
|
1890
|
+
// 成功 9 键(D-97-03-03):dry-run 只读 counts,零写盘
|
|
1891
|
+
console.log(JSON.stringify({
|
|
1892
|
+
schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log,
|
|
1893
|
+
dryRun: true, eligible: true, unfinishedStages: [], blockingReviews: [],
|
|
1894
|
+
}, null, 2));
|
|
1682
1895
|
return;
|
|
1683
1896
|
}
|
|
1684
1897
|
console.log(t('flow.archive.previewTmpl', lang, { version, stages: String(counts.stages), reviews: String(counts.reviews), log: String(counts.log) }));
|
|
@@ -1694,13 +1907,14 @@ export function registerFlowCommand(program) {
|
|
|
1694
1907
|
writeArchiveFile(projectPath, file);
|
|
1695
1908
|
}
|
|
1696
1909
|
catch (err) {
|
|
1697
|
-
// 写盘失败(REV-79-013):内存未改动(移除/索引/审计均在其后),无需回滚;exit
|
|
1910
|
+
// 写盘失败(REV-79-013):内存未改动(移除/索引/审计均在其后),无需回滚;exit 3 零 flow.json 变更
|
|
1911
|
+
// 分类补全(D-95-09):写盘失败用户无法通过修正输入解决 → internal / exit 3
|
|
1698
1912
|
if (options.json) {
|
|
1699
|
-
failJson({ error: 'archive-write-failed', version });
|
|
1913
|
+
failJson({ error: 'archive-write-failed', version }, 'internal', 3);
|
|
1700
1914
|
return;
|
|
1701
1915
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1916
|
+
const message = t('common.errorTmpl', lang, { msg: err instanceof Error ? err.message : String(err) });
|
|
1917
|
+
failCli({ code: 'ARCHIVE_WRITE_FAILED', message, detail: { version }, stderrLines: [message] });
|
|
1704
1918
|
return;
|
|
1705
1919
|
}
|
|
1706
1920
|
// ② 内存移除(stages / reviews / log 三段)
|
|
@@ -1727,7 +1941,11 @@ export function registerFlowCommand(program) {
|
|
|
1727
1941
|
return;
|
|
1728
1942
|
}
|
|
1729
1943
|
if (options.json) {
|
|
1730
|
-
|
|
1944
|
+
// 成功 9 键(D-97-03-03):保持在 mgr.save() 成功之后输出(顺序不变)
|
|
1945
|
+
console.log(JSON.stringify({
|
|
1946
|
+
schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log,
|
|
1947
|
+
dryRun: false, eligible: true, unfinishedStages: [], blockingReviews: [],
|
|
1948
|
+
}, null, 2));
|
|
1731
1949
|
return;
|
|
1732
1950
|
}
|
|
1733
1951
|
if (added) {
|
|
@@ -1794,30 +2012,33 @@ export function registerFlowCommand(program) {
|
|
|
1794
2012
|
const version = normalizeArchiveVersion(versionInput);
|
|
1795
2013
|
if (!version) {
|
|
1796
2014
|
if (json) {
|
|
1797
|
-
|
|
2015
|
+
// legacy 形态:error 恒 string、既有键序不变,errorKind 恒末尾
|
|
2016
|
+
console.log(JSON.stringify({ schemaVersion: 1, error: 'invalid-version', input: versionInput, available, errorKind: 'refusal' }, null, 2));
|
|
1798
2017
|
process.exitCode = 1;
|
|
1799
2018
|
return;
|
|
1800
2019
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
2020
|
+
const message = t('flow.archive.invalidVersionTmpl', lang, { input: versionInput });
|
|
2021
|
+
failCli({ code: 'INVALID_ARGUMENT', message, detail: { input: versionInput }, stderrLines: [message] });
|
|
1803
2022
|
return;
|
|
1804
2023
|
}
|
|
1805
2024
|
const file = readArchiveFile(projectPath, version);
|
|
1806
2025
|
if (!file) {
|
|
1807
2026
|
if (json) {
|
|
1808
|
-
|
|
2027
|
+
// legacy 形态:error 恒 string、既有键序不变,errorKind 恒末尾
|
|
2028
|
+
console.log(JSON.stringify({ schemaVersion: 1, error: 'not-found', version, available, errorKind: 'refusal' }, null, 2));
|
|
1809
2029
|
process.exitCode = 1;
|
|
1810
2030
|
return;
|
|
1811
2031
|
}
|
|
1812
|
-
|
|
2032
|
+
const message = t('flow.archive.show.notFoundTmpl', lang, {
|
|
1813
2033
|
version, versions: available.length > 0 ? available.join(', ') : t('common.none', lang),
|
|
1814
|
-
})
|
|
1815
|
-
|
|
2034
|
+
});
|
|
2035
|
+
failCli({ code: 'INVALID_ARGUMENT', message, detail: { version }, stderrLines: [message] });
|
|
1816
2036
|
return;
|
|
1817
2037
|
}
|
|
1818
2038
|
// --json = 全量载荷(ArchiveFile 原文自带 schemaVersion:1,含 log / reviews)——显式请求才取全量(REV-79-003)
|
|
2039
|
+
// G-1(stage-95 op-001):命令层 withSchemaVersion 包装,保证 schemaVersion 恒为首键;既有键相对顺序不变
|
|
1819
2040
|
if (json) {
|
|
1820
|
-
console.log(JSON.stringify(file, null, 2));
|
|
2041
|
+
console.log(JSON.stringify(withSchemaVersion(file), null, 2));
|
|
1821
2042
|
return;
|
|
1822
2043
|
}
|
|
1823
2044
|
console.log(t('flow.archive.show.titleTmpl', lang, { version, at: typeof file.archived_at === 'string' ? file.archived_at : '' }));
|
|
@@ -1863,10 +2084,9 @@ export function registerFlowCommand(program) {
|
|
|
1863
2084
|
reconciled = mgr.reconcileStatusMd({ dryRun: options.dryRun });
|
|
1864
2085
|
}
|
|
1865
2086
|
catch (err) {
|
|
1866
|
-
// 写盘失败 →
|
|
2087
|
+
// 写盘失败 → 内部错误(D-95-11:原 exit 1 → exit 3,登记为分类补全);非 JSON 文案逐字不变
|
|
1867
2088
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1868
|
-
|
|
1869
|
-
process.exit(1);
|
|
2089
|
+
failInternal(err, { stderrLines: [t('common.errorTmpl', lang, { msg })] });
|
|
1870
2090
|
return;
|
|
1871
2091
|
}
|
|
1872
2092
|
}
|
|
@@ -1879,11 +2099,8 @@ export function registerFlowCommand(program) {
|
|
|
1879
2099
|
...archiveItems.map((i) => ({ name: i.section, status: i.status, detail: i.message })),
|
|
1880
2100
|
...reqItems.map((i) => ({ name: i.section, status: i.status, detail: i.message })),
|
|
1881
2101
|
];
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
if (reconciled) {
|
|
1885
|
-
payload.reconciled = reconciled;
|
|
1886
|
-
}
|
|
2102
|
+
// D-95-02:reconciled 恒在(非 --fix 时 null),位置固定于 items 之后
|
|
2103
|
+
const payload = { schemaVersion: 1, ok: report.ok, items, reconciled: reconciled ?? null };
|
|
1887
2104
|
console.log(JSON.stringify(payload, null, 2));
|
|
1888
2105
|
if (!report.ok) {
|
|
1889
2106
|
process.exit(1);
|
|
@@ -1947,14 +2164,42 @@ export function registerFlowCommand(program) {
|
|
|
1947
2164
|
flow
|
|
1948
2165
|
.command('recover')
|
|
1949
2166
|
.description('跨会话上下文恢复:输出流水线状态、阻塞原因和待处理任务')
|
|
1950
|
-
.
|
|
2167
|
+
.option('--json', t('help.flow.recover.json', getCliLang(process.cwd())))
|
|
2168
|
+
.action((options) => {
|
|
1951
2169
|
const lang = getCliLang(process.cwd());
|
|
1952
2170
|
const mgr = createManager();
|
|
1953
2171
|
if (!mgr.isLoaded()) {
|
|
2172
|
+
// JSON 分支:未初始化 exit 0 + initialized:false + 空值(镜像人类 exit 0)
|
|
2173
|
+
if (options.json) {
|
|
2174
|
+
emitJson(withSchemaVersion({
|
|
2175
|
+
initialized: false,
|
|
2176
|
+
phase: null,
|
|
2177
|
+
currentOp: null,
|
|
2178
|
+
stageStatus: null,
|
|
2179
|
+
blockedBy: null,
|
|
2180
|
+
pendingTasks: [],
|
|
2181
|
+
stageStats: {},
|
|
2182
|
+
}));
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
1954
2185
|
console.log(t('common.noInit', lang));
|
|
1955
2186
|
return;
|
|
1956
2187
|
}
|
|
1957
2188
|
const recovery = mgr.recoverContext(lang);
|
|
2189
|
+
const stageStats = mgr.getAllStageStats();
|
|
2190
|
+
// JSON 分支(键序:initialized,phase,currentOp,stageStatus,blockedBy,pendingTasks,stageStats)
|
|
2191
|
+
if (options.json) {
|
|
2192
|
+
emitJson(withSchemaVersion({
|
|
2193
|
+
initialized: true,
|
|
2194
|
+
phase: recovery.phase,
|
|
2195
|
+
currentOp: recovery.currentOp,
|
|
2196
|
+
stageStatus: recovery.stageStatus,
|
|
2197
|
+
blockedBy: recovery.blockedBy === '' ? null : recovery.blockedBy,
|
|
2198
|
+
pendingTasks: recovery.pendingTasks,
|
|
2199
|
+
stageStats,
|
|
2200
|
+
}));
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
1958
2203
|
console.log('');
|
|
1959
2204
|
console.log(t('flow.recover.title', lang));
|
|
1960
2205
|
console.log('');
|
|
@@ -2171,9 +2416,10 @@ export function registerFlowCommand(program) {
|
|
|
2171
2416
|
}
|
|
2172
2417
|
}
|
|
2173
2418
|
catch (err) {
|
|
2174
|
-
// 并发写冲突:统一单点(i18n 文案 + 退出码 2
|
|
2419
|
+
// 并发写冲突:统一单点(i18n 文案 + 退出码 2);JSON 模式置码返回 → 显式 return 防落入通用分支
|
|
2175
2420
|
if (isFlowConcurrentError(err)) {
|
|
2176
2421
|
handleConcurrentConflict(err, lang);
|
|
2422
|
+
return;
|
|
2177
2423
|
}
|
|
2178
2424
|
// 失败:统一错误模板 + 非 0 退出(T39)
|
|
2179
2425
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2254,6 +2500,18 @@ function findPhaseEnteredAtMs(mgr, stageArg, fromPhase, logLenBefore) {
|
|
|
2254
2500
|
function createManager() {
|
|
2255
2501
|
return new FlowManager(process.cwd());
|
|
2256
2502
|
}
|
|
2503
|
+
/**
|
|
2504
|
+
* `--stage` 非法 / 不存在错误的 stderr 两行(逐字保持既有文案与顺序)。
|
|
2505
|
+
* @param lang 语言标识
|
|
2506
|
+
* @param projectPath 项目根
|
|
2507
|
+
* @param input 用户输入的阶段 ID
|
|
2508
|
+
*/
|
|
2509
|
+
function stageIdErrorLines(lang, projectPath, input) {
|
|
2510
|
+
return [
|
|
2511
|
+
t('common.stageIdInvalidTmpl', lang, { input }),
|
|
2512
|
+
t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, input) }),
|
|
2513
|
+
];
|
|
2514
|
+
}
|
|
2257
2515
|
/**
|
|
2258
2516
|
* 断言不存在阻塞中的 open REV(REV 闭环兜底,B5-3 / REV-52-001)。
|
|
2259
2517
|
* 命中 → 打印列出并返回命中的 REV 列表(调用方决定退出码与文案);无命中 → 空数组。
|
|
@@ -2267,8 +2525,8 @@ function assertNoBlockingOpenRev(mgr, stage, lang) {
|
|
|
2267
2525
|
return [];
|
|
2268
2526
|
}
|
|
2269
2527
|
// REV-007:补 `.` 分隔符(与 core flow-manager.ts 的 stageId + '.' 范式一致),避免 stageId 前缀重叠误拦
|
|
2270
|
-
|
|
2271
|
-
const blockingOpen =
|
|
2528
|
+
// 单点委托:阶段域 + blocking !== false + status === 'open' 判定归核心 listStageBlockingOpenReviews(禁第二份)
|
|
2529
|
+
const blockingOpen = listStageBlockingOpenReviews(mgr.getReviewItems(), stage);
|
|
2272
2530
|
if (blockingOpen.length > 0) {
|
|
2273
2531
|
console.warn(t('flow.advance.revBlockedTitleTmpl', lang, { n: String(blockingOpen.length) }));
|
|
2274
2532
|
for (const rev of blockingOpen) {
|
|
@@ -2313,9 +2571,21 @@ function collectOpStates(stage) {
|
|
|
2313
2571
|
}
|
|
2314
2572
|
return result;
|
|
2315
2573
|
}
|
|
2574
|
+
/**
|
|
2575
|
+
* 由 ops (opId→state) 派生汇总:total = 键数;byState 仅含观测到的状态(无零值键)。
|
|
2576
|
+
* 键序 = 状态首次出现序;空 ops → { total: 0, byState: {} }。
|
|
2577
|
+
*/
|
|
2578
|
+
function summarizeOpStates(ops) {
|
|
2579
|
+
const byState = {};
|
|
2580
|
+
for (const state of Object.values(ops)) {
|
|
2581
|
+
byState[state] = (byState[state] ?? 0) + 1;
|
|
2582
|
+
}
|
|
2583
|
+
return { total: Object.keys(ops).length, byState };
|
|
2584
|
+
}
|
|
2316
2585
|
/**
|
|
2317
2586
|
* 构建 `flow status --json` 的结构化输出(B1-1)。
|
|
2318
2587
|
* 顶层对象 + schemaVersion:1;stages 为数组,ops 为 opId→state 映射;counts 为阶段计数。
|
|
2588
|
+
* stage-96 op-002:stages[] 末位追加 5 个调度属性(既有 5 键键序/类型逐字不变)。
|
|
2319
2589
|
*/
|
|
2320
2590
|
function buildStatusJson(mgr) {
|
|
2321
2591
|
const data = mgr.getData();
|
|
@@ -2327,13 +2597,23 @@ function buildStatusJson(mgr) {
|
|
|
2327
2597
|
counts: { total: 0, done: 0, active: 0 },
|
|
2328
2598
|
};
|
|
2329
2599
|
}
|
|
2330
|
-
const stages = Object.entries(data.stages).map(([id, stage]) =>
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2600
|
+
const stages = Object.entries(data.stages).map(([id, stage]) => {
|
|
2601
|
+
const ops = collectOpStates(stage);
|
|
2602
|
+
// 模式单点解析:逐阶段经 resolveStageWorkflowMode(同一 manager 快照;D-96-12 不缓存)
|
|
2603
|
+
const workflowMode = mgr.resolveStageWorkflowMode(id).value;
|
|
2604
|
+
return {
|
|
2605
|
+
id,
|
|
2606
|
+
phase: stage.phase,
|
|
2607
|
+
status: stage.status,
|
|
2608
|
+
deps: stage.deps ?? [],
|
|
2609
|
+
ops,
|
|
2610
|
+
workflowMode,
|
|
2611
|
+
requirementId: stage.requirement_id ?? null,
|
|
2612
|
+
reviewTier: MODE_DERIVED_TIER[workflowMode],
|
|
2613
|
+
effectiveReviewTier: DEFAULT_EFFECTIVE_REVIEW_TIER,
|
|
2614
|
+
opsSummary: summarizeOpStates(ops),
|
|
2615
|
+
};
|
|
2616
|
+
});
|
|
2337
2617
|
const total = stages.length;
|
|
2338
2618
|
const done = stages.filter((s) => s.phase === 'done').length;
|
|
2339
2619
|
return {
|