openfeel 1.1.5 → 1.1.7
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 -5
- package/README.md +22 -3
- package/README.zh-CN.md +26 -5
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +66 -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 +169 -10
- package/dist/commands/flow.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +59 -2
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +24 -2
- package/dist/commands/setup.js.map +1 -1
- 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/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +16 -4
- package/dist/commands/update.js.map +1 -1
- package/dist/core/backup.d.ts.map +1 -1
- package/dist/core/backup.js +18 -5
- package/dist/core/backup.js.map +1 -1
- package/dist/core/claude-settings.d.ts +31 -0
- package/dist/core/claude-settings.d.ts.map +1 -0
- package/dist/core/claude-settings.js +120 -0
- package/dist/core/claude-settings.js.map +1 -0
- 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-manager.d.ts +71 -14
- package/dist/core/flow-manager.d.ts.map +1 -1
- package/dist/core/flow-manager.js +266 -51
- package/dist/core/flow-manager.js.map +1 -1
- package/dist/core/global-paths.d.ts +10 -0
- package/dist/core/global-paths.d.ts.map +1 -1
- package/dist/core/global-paths.js +21 -0
- package/dist/core/global-paths.js.map +1 -1
- package/dist/core/i18n-data/en.d.ts.map +1 -1
- package/dist/core/i18n-data/en.js +44 -9
- 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 +45 -9
- package/dist/core/i18n-data/zh-CN.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/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/setup.d.ts +2 -2
- package/dist/core/setup.d.ts.map +1 -1
- package/dist/core/setup.js +81 -53
- package/dist/core/setup.js.map +1 -1
- package/dist/core/template-loader.d.ts +15 -0
- package/dist/core/template-loader.d.ts.map +1 -1
- package/dist/core/template-loader.js +4218 -145
- package/dist/core/template-loader.js.map +1 -1
- package/dist/core/update.d.ts +8 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +368 -141
- package/dist/core/update.js.map +1 -1
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, readd
|
|
|
13
13
|
import { resolve, dirname, basename } from 'node:path';
|
|
14
14
|
import { execFileSync } from 'node:child_process';
|
|
15
15
|
import { parse as parseYaml } from 'yaml';
|
|
16
|
-
import { PipelineConfigSchema, PipelinePhaseSchema, PIPELINE_PHASES, MetaPhaseSchema, META_PHASES, transitionKeyMatches, } from './pipeline-schema.js';
|
|
16
|
+
import { PipelineConfigSchema, PipelinePhaseSchema, PIPELINE_PHASES, MetaPhaseSchema, META_PHASES, transitionKeyMatches, FastTransitionsSchema, } from './pipeline-schema.js';
|
|
17
17
|
import { PublicLogger, formatDate } from './public-logger.js';
|
|
18
18
|
import { t, getCliLang } from './i18n.js';
|
|
19
19
|
import { findStageStatusPath, findStageDirConflict, parseStageId, normalizeStageId } from './plan/path.js';
|
|
@@ -52,8 +52,8 @@ export class StageDirConflictError extends Error {
|
|
|
52
52
|
this.other = other;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
/** `openfeel config effective`
|
|
56
|
-
const EFFECTIVE_CONFIG_KEYS = ['execution_mode', 'auto_advance', 'merge_mode'];
|
|
55
|
+
/** `openfeel config effective` 的四个受管键(与 DEFAULT_CONFIG 一致) */
|
|
56
|
+
const EFFECTIVE_CONFIG_KEYS = ['execution_mode', 'auto_advance', 'merge_mode', 'workflow_mode'];
|
|
57
57
|
// ── 默认值 ──
|
|
58
58
|
/** 创建默认的 Checkpoints */
|
|
59
59
|
function defaultCheckpoints() {
|
|
@@ -163,6 +163,23 @@ export function findOrphanOps(projectPath) {
|
|
|
163
163
|
}
|
|
164
164
|
/** findPhasePath BFS 深度上限(B5-1;超出按拒绝处理) */
|
|
165
165
|
export const PHASE_PATH_MAX_DEPTH = 8;
|
|
166
|
+
/**
|
|
167
|
+
* fast 模式内置快线转移表(裁定 C):8 条流转键 + `done` 终止项(共 9 条目)。
|
|
168
|
+
* 单一来源:仅 getDefaultPipelineConfig('fast') 消费;禁止在命令层/模板层复制(R-74-1)。
|
|
169
|
+
* `plan_review` 在 fast 下必经(plan_pending → plan_review → plan_passed;通过 → plan_passed,问题退回 → plan_pending)。
|
|
170
|
+
* review_failed 双出口 = 失败回环(重审 或 回执行修复,对应 full 表 scheme_pending 位);Q3 收紧项留审查裁定。
|
|
171
|
+
*/
|
|
172
|
+
const FAST_TRANSITIONS = {
|
|
173
|
+
plan_pending: ['plan_review'],
|
|
174
|
+
plan_review: ['plan_passed', 'plan_pending'],
|
|
175
|
+
plan_passed: ['exec_running'],
|
|
176
|
+
exec_running: ['review_pending'],
|
|
177
|
+
review_pending: ['review_failed', 'review_passed'],
|
|
178
|
+
review_failed: ['review_pending', 'exec_running'],
|
|
179
|
+
review_passed: ['archiving'],
|
|
180
|
+
archiving: ['done'],
|
|
181
|
+
done: [],
|
|
182
|
+
};
|
|
166
183
|
// ── 核心类 ──
|
|
167
184
|
export class FlowManager {
|
|
168
185
|
projectPath;
|
|
@@ -172,6 +189,8 @@ export class FlowManager {
|
|
|
172
189
|
filePath;
|
|
173
190
|
/** 从 pipeline.yaml 加载的流水线配置(含后备默认值) */
|
|
174
191
|
pipelineConfig = null;
|
|
192
|
+
/** 从 pipeline.yaml 加载的可选快线表(fast_transitions);null = 缺失/非法/空对象 → 回退内置快线表 [REV-74-009 修订] */
|
|
193
|
+
fastTransitions = null;
|
|
175
194
|
/** 公共日志写入器(审计链) */
|
|
176
195
|
publicLogger;
|
|
177
196
|
constructor(projectPath) {
|
|
@@ -1477,10 +1496,11 @@ export class FlowManager {
|
|
|
1477
1496
|
* 遍历 transitions 表,key 与 fromPhase 匹配(组合 key 如 'test_passed|review_passed' 任一条件匹配即可)即收集其目标。
|
|
1478
1497
|
* 无 '|' 的简单 key 保持单条件原行为(精确相等)。
|
|
1479
1498
|
* @param fromPhase 当前源 phase
|
|
1499
|
+
* @param mode 工作流模式(**必传**;由调用点经 resolveStageWorkflowMode 从 stage 派生——禁止默认 mode)
|
|
1480
1500
|
* @returns 去重后的可达目标 phase 列表
|
|
1481
1501
|
*/
|
|
1482
|
-
getValidTargets(fromPhase) {
|
|
1483
|
-
const transitions = this.
|
|
1502
|
+
getValidTargets(fromPhase, mode) {
|
|
1503
|
+
const transitions = this.resolveTransitionsForMode(mode);
|
|
1484
1504
|
const result = new Set();
|
|
1485
1505
|
for (const [key, targets] of Object.entries(transitions)) {
|
|
1486
1506
|
if (transitionKeyMatches(key, fromPhase)) {
|
|
@@ -1503,7 +1523,8 @@ export class FlowManager {
|
|
|
1503
1523
|
const phase = this.resolveCurrentPhase(stageName);
|
|
1504
1524
|
if (!phase)
|
|
1505
1525
|
return false;
|
|
1506
|
-
|
|
1526
|
+
const mode = this.resolveStageWorkflowMode(stageName).value;
|
|
1527
|
+
return this.getValidTargets(phase, mode).includes(to);
|
|
1507
1528
|
}
|
|
1508
1529
|
/**
|
|
1509
1530
|
* 获取当前阶段的所有可达下一阶段(基于 transitions 表,支持组合条件)
|
|
@@ -1520,7 +1541,8 @@ export class FlowManager {
|
|
|
1520
1541
|
const currentPhase = this.resolveCurrentPhase(stageName);
|
|
1521
1542
|
if (!currentPhase)
|
|
1522
1543
|
return [];
|
|
1523
|
-
|
|
1544
|
+
const mode = this.resolveStageWorkflowMode(stageName).value;
|
|
1545
|
+
return this.getValidTargets(currentPhase, mode);
|
|
1524
1546
|
}
|
|
1525
1547
|
/**
|
|
1526
1548
|
* 在运行时 transitions 上求 stageName 当前 phase → to 的**唯一**可达路径(B5-1)。
|
|
@@ -1542,6 +1564,8 @@ export class FlowManager {
|
|
|
1542
1564
|
if (start === to) {
|
|
1543
1565
|
return { path: [], reason: 'already-at-target' };
|
|
1544
1566
|
}
|
|
1567
|
+
// 模式在 BFS 外解析一次(stage 派生;同一 T 内 mode 恒定——单 stage 只读一张表)
|
|
1568
|
+
const mode = this.resolveStageWorkflowMode(key).value;
|
|
1545
1569
|
// BFS 逐层扩展;visitedDepth 记录各 phase 首次到达的最小深度(剪枝,避免重复/环)
|
|
1546
1570
|
const visitedDepth = new Map();
|
|
1547
1571
|
visitedDepth.set(start, 0);
|
|
@@ -1554,7 +1578,7 @@ export class FlowManager {
|
|
|
1554
1578
|
if (node.path.length >= PHASE_PATH_MAX_DEPTH) {
|
|
1555
1579
|
continue;
|
|
1556
1580
|
}
|
|
1557
|
-
for (const target of this.getValidTargets(node.phase)) {
|
|
1581
|
+
for (const target of this.getValidTargets(node.phase, mode)) {
|
|
1558
1582
|
const path = [...node.path, target];
|
|
1559
1583
|
if (target === to) {
|
|
1560
1584
|
hits.push(path);
|
|
@@ -1591,21 +1615,34 @@ export class FlowManager {
|
|
|
1591
1615
|
const phases = this.pipelineConfig?.phases ?? this.getDefaultPipelineConfig().phases;
|
|
1592
1616
|
return [...phases];
|
|
1593
1617
|
}
|
|
1618
|
+
/**
|
|
1619
|
+
* 按模式解析运行时转移表(唯一取表法则;校验/推进/路径 与 展示 同经本函数 ⇒ 单一信源)。
|
|
1620
|
+
* full → 运行时 pipelineConfig.transitions(pipeline.yaml 覆盖线);fast → fast_transitions ?? 内置快线表。
|
|
1621
|
+
* 说明:pipelineConfig 构造期恒已加载(constructor 无条件调用),`?? 内置` 保留既有访问器回退语义。
|
|
1622
|
+
*/
|
|
1623
|
+
resolveTransitionsForMode(mode) {
|
|
1624
|
+
if (mode === 'fast') {
|
|
1625
|
+
return this.fastTransitions ?? this.getDefaultPipelineConfig('fast').transitions;
|
|
1626
|
+
}
|
|
1627
|
+
return this.pipelineConfig?.transitions ?? this.getDefaultPipelineConfig().transitions;
|
|
1628
|
+
}
|
|
1594
1629
|
/**
|
|
1595
1630
|
* 获取运行时生效的 phase 转移表(key 可含 '|' 组合条件)。
|
|
1596
1631
|
* 缺省/解析失败时回退内置默认配置。返回浅拷贝。
|
|
1632
|
+
* **无参 = full**(零回归既有消费点);fast 供 stage-75 `flow phases` 模式分节消费。
|
|
1597
1633
|
*/
|
|
1598
|
-
getPipelineTransitions() {
|
|
1599
|
-
const transitions = this.
|
|
1634
|
+
getPipelineTransitions(mode = 'full') {
|
|
1635
|
+
const transitions = this.resolveTransitionsForMode(mode);
|
|
1600
1636
|
return Object.fromEntries(Object.entries(transitions).map(([k, v]) => [k, [...v]]));
|
|
1601
1637
|
}
|
|
1602
1638
|
/**
|
|
1603
1639
|
* 获取内置默认 phase 转移表(不受项目 pipeline.yaml 影响)。
|
|
1604
1640
|
* 供 `flow phases` 做「运行时 vs 默认」差异报告(T19)。
|
|
1641
|
+
* **无参 = full**(零回归既有消费点);fast 供 stage-75 `flow phases` 模式分节消费。
|
|
1605
1642
|
* @returns 默认 transitions 的深拷贝
|
|
1606
1643
|
*/
|
|
1607
|
-
getDefaultTransitions() {
|
|
1608
|
-
return Object.fromEntries(Object.entries(this.getDefaultPipelineConfig().transitions).map(([k, v]) => [k, [...v]]));
|
|
1644
|
+
getDefaultTransitions(mode = 'full') {
|
|
1645
|
+
return Object.fromEntries(Object.entries(this.getDefaultPipelineConfig(mode).transitions).map(([k, v]) => [k, [...v]]));
|
|
1609
1646
|
}
|
|
1610
1647
|
/**
|
|
1611
1648
|
* 解析当前阶段 phase:优先使用传入的 stageName,其次 current.stage,最后 fallback
|
|
@@ -1698,10 +1735,13 @@ export class FlowManager {
|
|
|
1698
1735
|
}
|
|
1699
1736
|
/**
|
|
1700
1737
|
* 构建配置级联信息(优先级:profileDefaults < configDefaults < statusOverrides)
|
|
1701
|
-
* 全局画像仅作最低优先级兜底(P2),且只取 preferences.auto_advance
|
|
1702
|
-
*
|
|
1738
|
+
* 全局画像仅作最低优先级兜底(P2),且只取 preferences.auto_advance 单值,**不扩展** workflow_mode。
|
|
1739
|
+
* @param stageId 可选目标阶段(stage 形参唯一入口);缺省 = pipeline.current.stage
|
|
1740
|
+
* (**当前阶段域**,无参行为逐字不变)。任意阶段域由 resolveStageWorkflowMode(stageId) 显式传入。
|
|
1741
|
+
* 静态约束(REV-74-001/002):无参调用**仅允许**出现在既有两出口 resolveEffectiveConfig()
|
|
1742
|
+
* 与 verboseSummary();新增消费点一律显式传 stage。
|
|
1703
1743
|
*/
|
|
1704
|
-
buildCascadeConfig() {
|
|
1744
|
+
buildCascadeConfig(stageId) {
|
|
1705
1745
|
const profileDefaults = {};
|
|
1706
1746
|
const configDefaults = {};
|
|
1707
1747
|
const statusOverrides = {};
|
|
@@ -1752,22 +1792,26 @@ export class FlowManager {
|
|
|
1752
1792
|
// 解析失败则用空值
|
|
1753
1793
|
}
|
|
1754
1794
|
}
|
|
1755
|
-
//
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
const statusPath = this.findStatusPath(
|
|
1795
|
+
// 最高优先级:目标 stage 的 status.md(缺省 = pipeline.current.stage;stage 形参路径读该阶段)
|
|
1796
|
+
const targetStage = stageId ?? this.data?.pipeline.current.stage;
|
|
1797
|
+
if (targetStage) {
|
|
1798
|
+
const statusPath = this.findStatusPath(targetStage);
|
|
1759
1799
|
if (statusPath) {
|
|
1760
1800
|
try {
|
|
1761
1801
|
const content = readFileSync(statusPath, 'utf-8');
|
|
1762
|
-
//
|
|
1802
|
+
// 匹配执行模式、自动推进、工作流模式
|
|
1763
1803
|
const execMatch = content.match(/\*\*执行模式\*\*[::]\s*(manual|auto)/);
|
|
1764
1804
|
const autoMatch = content.match(/\*\*自动推进\*\*[::]\s*(disabled|enabled)/);
|
|
1805
|
+
const modeMatch = content.match(/\*\*工作流模式\*\*[::]\s*(full|fast)/);
|
|
1765
1806
|
if (execMatch) {
|
|
1766
1807
|
statusOverrides['execution_mode'] = execMatch[1];
|
|
1767
1808
|
}
|
|
1768
1809
|
if (autoMatch) {
|
|
1769
1810
|
statusOverrides['auto_advance'] = autoMatch[1];
|
|
1770
1811
|
}
|
|
1812
|
+
if (modeMatch) {
|
|
1813
|
+
statusOverrides['workflow_mode'] = modeMatch[1];
|
|
1814
|
+
}
|
|
1771
1815
|
}
|
|
1772
1816
|
catch {
|
|
1773
1817
|
// 读取失败则跳过
|
|
@@ -1779,7 +1823,7 @@ export class FlowManager {
|
|
|
1779
1823
|
return { profileDefaults, configDefaults, statusOverrides, effective };
|
|
1780
1824
|
}
|
|
1781
1825
|
/**
|
|
1782
|
-
*
|
|
1826
|
+
* 解析四个受管配置键的「有效值 + 生效来源」(`openfeel config effective`,P9)。
|
|
1783
1827
|
* 复用 buildCascadeConfig(单一权威,避免第二套解析),优先级:
|
|
1784
1828
|
* status.md > config.yaml > profile.yaml > builtin。
|
|
1785
1829
|
* @returns key → { value, source }
|
|
@@ -1804,6 +1848,30 @@ export class FlowManager {
|
|
|
1804
1848
|
}
|
|
1805
1849
|
return result;
|
|
1806
1850
|
}
|
|
1851
|
+
/**
|
|
1852
|
+
* 解析指定阶段的工作流模式(裁定 E:「stage → mode」表选择解析入口;stage-75 `flow mode` 契约)。
|
|
1853
|
+
* 带 stage 形参独立解析(REV-74-001):该阶段 status.md(findStatusPath(stageId))
|
|
1854
|
+
* > config.yaml defaults > profile.yaml(**有意不承载**,实际不产出)> builtin(DEFAULT_CONFIG.workflow_mode = fast)。
|
|
1855
|
+
* 语义边界:`config effective` / `flow status --verbose` = **当前阶段域**(无参);
|
|
1856
|
+
* 本方法显式 stageId = **任意阶段域**(flow mode --stage / 表选择)。
|
|
1857
|
+
* @param stageId 目标阶段 ID(缺省 = pipeline.current.stage;无该阶段 status.md → 自然回落下一层)
|
|
1858
|
+
* @returns { value, source },source ∈ status.md | config.yaml | profile.yaml | builtin
|
|
1859
|
+
*/
|
|
1860
|
+
resolveStageWorkflowMode(stageId) {
|
|
1861
|
+
const cascade = this.buildCascadeConfig(stageId);
|
|
1862
|
+
const key = 'workflow_mode';
|
|
1863
|
+
if (cascade.statusOverrides[key] !== undefined) {
|
|
1864
|
+
return { value: cascade.statusOverrides[key], source: 'status.md' };
|
|
1865
|
+
}
|
|
1866
|
+
if (cascade.configDefaults[key] !== undefined) {
|
|
1867
|
+
return { value: cascade.configDefaults[key], source: 'config.yaml' };
|
|
1868
|
+
}
|
|
1869
|
+
if (cascade.profileDefaults[key] !== undefined) {
|
|
1870
|
+
// profile 层有意不承载 workflow_mode(画像 = 个人偏好层,REV-74-005);此分支为对称性保留,实际不产出
|
|
1871
|
+
return { value: cascade.profileDefaults[key], source: 'profile.yaml' };
|
|
1872
|
+
}
|
|
1873
|
+
return { value: DEFAULT_CONFIG.workflow_mode, source: 'builtin' };
|
|
1874
|
+
}
|
|
1807
1875
|
/** 查找 status.md 的路径(三级回退:plan/{series}/ 精确 → plan 递归 → stages 兜底) */
|
|
1808
1876
|
findStatusPath(stageId) {
|
|
1809
1877
|
return findStageStatusPath(this.projectPath, stageId);
|
|
@@ -1861,7 +1929,7 @@ export class FlowManager {
|
|
|
1861
1929
|
// 返回最近 N 条(倒序取最后)
|
|
1862
1930
|
return results.slice(-maxChanges);
|
|
1863
1931
|
}
|
|
1864
|
-
/**
|
|
1932
|
+
/** 构建下游阶段列表(当前阶段的可达下一阶段 + Agent 映射;模式经 getAvailablePhases() 无参 → current.stage 派生) */
|
|
1865
1933
|
buildDownstreamPhases() {
|
|
1866
1934
|
const available = this.getAvailablePhases();
|
|
1867
1935
|
const labels = this.getPhaseLabels();
|
|
@@ -2187,8 +2255,10 @@ export class FlowManager {
|
|
|
2187
2255
|
return false;
|
|
2188
2256
|
}
|
|
2189
2257
|
const currentPhase = this.data.stages[parts.stageId].phase;
|
|
2258
|
+
// stage 派生 mode:opId 解析出的 stage(src 内无生产调用方,仅测试;仍为 mode 透传必改点)
|
|
2259
|
+
const mode = this.resolveStageWorkflowMode(parts.stageId).value;
|
|
2190
2260
|
// 检查从当前 phase 到目标 phase 的流转是否合法(支持组合条件 key)
|
|
2191
|
-
const validTargets = this.getValidTargets(currentPhase);
|
|
2261
|
+
const validTargets = this.getValidTargets(currentPhase, mode);
|
|
2192
2262
|
if (!validTargets.includes(to)) {
|
|
2193
2263
|
return false;
|
|
2194
2264
|
}
|
|
@@ -2817,6 +2887,10 @@ export class FlowManager {
|
|
|
2817
2887
|
if (!quick) {
|
|
2818
2888
|
this.checkEmptyTemplates(items);
|
|
2819
2889
|
}
|
|
2890
|
+
// ── 10. 工作流模式冗余字段一致性(stage-75 op-003;仅 warn,不 fail、不自动修复)──
|
|
2891
|
+
if (!quick) {
|
|
2892
|
+
this.checkWorkflowModeRedundancy(items);
|
|
2893
|
+
}
|
|
2820
2894
|
const ok = items.every((i) => i.status !== 'fail');
|
|
2821
2895
|
return { items, ok };
|
|
2822
2896
|
}
|
|
@@ -2902,23 +2976,84 @@ export class FlowManager {
|
|
|
2902
2976
|
return true;
|
|
2903
2977
|
});
|
|
2904
2978
|
}
|
|
2979
|
+
/**
|
|
2980
|
+
* status.md 字段「插入」写(原子写 + 按阶段目录加锁)。
|
|
2981
|
+
* 与 `writeStatusField`(替换语义)并列的**独立**实现,禁双重语义:
|
|
2982
|
+
* 字段行已存在 → `exists`(不写盘);锚点链「自动推进」后 → 回退「执行模式」后 → 皆缺 `no-anchor`(不写盘 / 无 `.bak`)。
|
|
2983
|
+
* 备份 `.openfeel/tmp/status.{stageId}.bak` 仅在**确认插入**时生成。
|
|
2984
|
+
* 不依赖 `this.data`(flow.json 未加载的存量项目也可仅凭 `this.projectPath` 定位锁/备份)。
|
|
2985
|
+
* @param statusPath status.md 绝对路径
|
|
2986
|
+
* @param stageId 备份文件名中的阶段 ID(单阶段 = 用户输入原值;批量 = flow.json 注册键)
|
|
2987
|
+
* @param key 字段名(如「工作流模式」)
|
|
2988
|
+
* @param value 目标值
|
|
2989
|
+
* @returns `inserted` 已插入 | `exists` 字段已存在(竞态防御,调用方可回退替换)| `no-anchor` 锚点缺失
|
|
2990
|
+
*/
|
|
2991
|
+
insertStatusField(statusPath, stageId, key, value) {
|
|
2992
|
+
const stageDir = basename(dirname(statusPath));
|
|
2993
|
+
const lockPath = projectLockPath(this.projectPath, `status-${stageDir}`);
|
|
2994
|
+
return withFileLock(lockPath, () => {
|
|
2995
|
+
const content = readFileSync(statusPath, 'utf-8');
|
|
2996
|
+
// 锁内复查:字段行已存在 → 不写盘(探针与写之间的竞态防御)
|
|
2997
|
+
const fieldRegex = new RegExp(`^-\\s*(?:\\*\\*)?${escapeRegex(key)}(?:\\*\\*)?[::]`, 'm');
|
|
2998
|
+
if (fieldRegex.test(content)) {
|
|
2999
|
+
return 'exists';
|
|
3000
|
+
}
|
|
3001
|
+
// 锚点链:「自动推进」行后优先;缺失则回退「执行模式」行后;皆缺 = no-anchor
|
|
3002
|
+
const anchor = content.match(/^-\s*(?:\*\*)?自动推进(?:\*\*)?[::]/m) ??
|
|
3003
|
+
content.match(/^-\s*(?:\*\*)?执行模式(?:\*\*)?[::]/m);
|
|
3004
|
+
if (!anchor || anchor.index === undefined) {
|
|
3005
|
+
return 'no-anchor';
|
|
3006
|
+
}
|
|
3007
|
+
const newLine = `- **${key}**:${value}`;
|
|
3008
|
+
const lineEnd = content.indexOf('\n', anchor.index + anchor[0].length);
|
|
3009
|
+
let updated;
|
|
3010
|
+
if (lineEnd === -1) {
|
|
3011
|
+
// 错误路径:锚点行位于文件末行(无尾换行)→ 换行后追加
|
|
3012
|
+
updated = `${content}\n${newLine}`;
|
|
3013
|
+
}
|
|
3014
|
+
else {
|
|
3015
|
+
// 保留锚点行原行尾风格(CRLF 感知),其余字节逐字保留
|
|
3016
|
+
const isCrlf = content[lineEnd - 1] === '\r';
|
|
3017
|
+
const eol = isCrlf ? '\r\n' : '\n';
|
|
3018
|
+
updated = `${content.slice(0, lineEnd + 1)}${newLine}${eol}${content.slice(lineEnd + 1)}`;
|
|
3019
|
+
}
|
|
3020
|
+
// 备份仅在确认插入时生成
|
|
3021
|
+
const tmpDir = resolve(this.projectPath, '.openfeel', 'tmp');
|
|
3022
|
+
if (!existsSync(tmpDir)) {
|
|
3023
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
3024
|
+
}
|
|
3025
|
+
copyFileSync(statusPath, resolve(tmpDir, `status.${stageId}.bak`));
|
|
3026
|
+
atomicWriteFileSync(statusPath, updated);
|
|
3027
|
+
return 'inserted';
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
2905
3030
|
/**
|
|
2906
3031
|
* 将受管配置键批量同步到所有已注册阶段的 status.md 对应字段(`config set --sync-stages`)。
|
|
2907
|
-
* 仅 auto_advance→「自动推进」、execution_mode
|
|
2908
|
-
*
|
|
2909
|
-
*
|
|
2910
|
-
*
|
|
3032
|
+
* 仅 auto_advance→「自动推进」、execution_mode→「执行模式」、workflow_mode→「工作流模式」
|
|
3033
|
+
* 三个映射键有阶段级字段;其它键无阶段字段 → 全部 skipped-no-field(D-config 裁定,跳过并报告)。
|
|
3034
|
+
* `workflow_mode` 字段缺失时复用 `insertStatusField`(插入语义,单一实现),锚点缺失 → skipped-no-anchor。
|
|
3035
|
+
* 复用 writeStatusField / insertStatusField 的定向写 + 按阶段锁;同值 → noop(零写盘)。
|
|
3036
|
+
* **冗余写入(裁定 G)**:`workflow_mode` 实际写入(updated / inserted)时同步 `stages[id].workflow_mode`
|
|
3037
|
+
* 并 save 一次;noop / skipped 全量运行 → flow.json 零写盘。
|
|
2911
3038
|
* @param configKey 已归一的受管配置键(如 auto_advance)
|
|
2912
3039
|
* @param value 目标值
|
|
2913
|
-
* @
|
|
3040
|
+
* @param options 可选作用域:`onlyStages` 限定阶段 ID 列表(已归一化;缺省 = 全量现状)
|
|
3041
|
+
* @returns 每阶段一条结果(updated/inserted/noop/skipped-no-field/skipped-no-status/skipped-no-anchor)
|
|
2914
3042
|
*/
|
|
2915
|
-
syncConfigFieldToStages(configKey, value) {
|
|
3043
|
+
syncConfigFieldToStages(configKey, value, options) {
|
|
2916
3044
|
const out = [];
|
|
2917
3045
|
if (!this.data) {
|
|
2918
3046
|
return out;
|
|
2919
3047
|
}
|
|
2920
3048
|
const field = STAGE_FIELD_BY_CONFIG_KEY[configKey];
|
|
3049
|
+
const targets = options?.onlyStages;
|
|
3050
|
+
// 冗余写入标记:仅实际写入(updated / inserted)时置位,循环后 save 一次(D6)
|
|
3051
|
+
let flowJsonDirty = false;
|
|
2921
3052
|
for (const stageId of Object.keys(this.data.stages)) {
|
|
3053
|
+
// --stage 作用域限定:归一化比较兼容存量短名键(D5;未注册 id 自然落 skipped-no-status)
|
|
3054
|
+
if (targets && !targets.includes(normalizeStageId(stageId) ?? stageId)) {
|
|
3055
|
+
continue;
|
|
3056
|
+
}
|
|
2922
3057
|
// 错误路径:该 config 键无阶段级字段 → 跳过并报告
|
|
2923
3058
|
if (!field) {
|
|
2924
3059
|
out.push({ stage: stageId, result: 'skipped-no-field' });
|
|
@@ -2931,8 +3066,31 @@ export class FlowManager {
|
|
|
2931
3066
|
continue;
|
|
2932
3067
|
}
|
|
2933
3068
|
const current = readStatusFieldValue(readFileSync(statusPath, 'utf-8'), field);
|
|
2934
|
-
// 错误路径:字段行缺失 →
|
|
3069
|
+
// 错误路径:字段行缺失 →
|
|
3070
|
+
// workflow_mode 走「插入」语义(复用 op-001 单一实现);其余键保持 skipped-no-field(零回归)
|
|
2935
3071
|
if (current === null) {
|
|
3072
|
+
if (configKey === 'workflow_mode') {
|
|
3073
|
+
const r = this.insertStatusField(statusPath, stageId, field, value);
|
|
3074
|
+
if (r === 'inserted') {
|
|
3075
|
+
this.appendLog({ time: '', agent: 'cli', action: 'config_sync_stage', detail: { stage: stageId, field, value } });
|
|
3076
|
+
out.push({ stage: stageId, result: 'inserted' });
|
|
3077
|
+
this.data.stages[stageId].workflow_mode = value;
|
|
3078
|
+
flowJsonDirty = true;
|
|
3079
|
+
continue;
|
|
3080
|
+
}
|
|
3081
|
+
if (r === 'exists') {
|
|
3082
|
+
// 竞态防御:锁内发现字段已就位 → 回退替换语义(不产生双行)
|
|
3083
|
+
this.writeStatusField(statusPath, field, value);
|
|
3084
|
+
this.appendLog({ time: '', agent: 'cli', action: 'config_sync_stage', detail: { stage: stageId, field, value } });
|
|
3085
|
+
out.push({ stage: stageId, result: 'updated' });
|
|
3086
|
+
this.data.stages[stageId].workflow_mode = value;
|
|
3087
|
+
flowJsonDirty = true;
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
// 锚点链均缺失 → 跳过类(不写盘、不审计)
|
|
3091
|
+
out.push({ stage: stageId, result: 'skipped-no-anchor' });
|
|
3092
|
+
continue;
|
|
3093
|
+
}
|
|
2936
3094
|
out.push({ stage: stageId, result: 'skipped-no-field' });
|
|
2937
3095
|
continue;
|
|
2938
3096
|
}
|
|
@@ -2944,6 +3102,15 @@ export class FlowManager {
|
|
|
2944
3102
|
this.writeStatusField(statusPath, field, value);
|
|
2945
3103
|
this.appendLog({ time: '', agent: 'cli', action: 'config_sync_stage', detail: { stage: stageId, field, value } });
|
|
2946
3104
|
out.push({ stage: stageId, result: 'updated' });
|
|
3105
|
+
// 冗余写入点③:仅实际写入时同步(noop / skipped 不写)
|
|
3106
|
+
if (configKey === 'workflow_mode') {
|
|
3107
|
+
this.data.stages[stageId].workflow_mode = value;
|
|
3108
|
+
flowJsonDirty = true;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
// 仅在实际发生写入时保存(幂等:全 noop → flow.json 零写盘)
|
|
3112
|
+
if (flowJsonDirty) {
|
|
3113
|
+
this.save();
|
|
2947
3114
|
}
|
|
2948
3115
|
return out;
|
|
2949
3116
|
}
|
|
@@ -2977,6 +3144,37 @@ export class FlowManager {
|
|
|
2977
3144
|
}
|
|
2978
3145
|
items.push({ section: '悬空依赖', status: 'pass', message: '未检测到悬空依赖' });
|
|
2979
3146
|
}
|
|
3147
|
+
/**
|
|
3148
|
+
* 10. 工作流模式冗余字段一致性:flow.json `stages[].workflow_mode` 与级联解析值不一致 → warn(stage-75 op-003)。
|
|
3149
|
+
* 冗余字段不参与级联(裁定 G),读取权威恒为 status.md/级联;字段缺失(旧 flow.json)零报错;
|
|
3150
|
+
* 仅告警不 fail、不自动修复。无不一致时不产生条目(保持输出精简)——与 checkOrphanOps 同风格。
|
|
3151
|
+
* @param items 健康检查结果收集数组
|
|
3152
|
+
*/
|
|
3153
|
+
checkWorkflowModeRedundancy(items) {
|
|
3154
|
+
if (!this.data) {
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
const mismatched = [];
|
|
3158
|
+
for (const [stageId, stage] of Object.entries(this.data.stages)) {
|
|
3159
|
+
const redundant = stage.workflow_mode;
|
|
3160
|
+
// 错误路径:旧 flow.json 无该字段 → 直接跳过(零报错)
|
|
3161
|
+
if (redundant === undefined) {
|
|
3162
|
+
continue;
|
|
3163
|
+
}
|
|
3164
|
+
const resolved = this.resolveStageWorkflowMode(stageId).value;
|
|
3165
|
+
if (redundant !== resolved) {
|
|
3166
|
+
mismatched.push(`${stageId}(冗余 ${redundant} ≠ 解析 ${resolved})`);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
if (mismatched.length === 0) {
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
items.push({
|
|
3173
|
+
section: '工作流模式',
|
|
3174
|
+
status: 'warn',
|
|
3175
|
+
message: `检出 ${mismatched.length} 个阶段的 flow.json 冗余 workflow_mode 与权威解析值不一致(冗余不参与级联,可执行 openfeel config set workflow_mode <v> --sync-stages 修复):${mismatched.slice(0, 5).join('; ')}${mismatched.length > 5 ? ' …' : ''}`,
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
2980
3178
|
/**
|
|
2981
3179
|
* 8. 孤儿 op 检测:flow.json 的 op 键与 ops/ 目录文件是否一致(N1-3)
|
|
2982
3180
|
* 仅作数据卫生告警(warn),不阻断;退出码不受影响(仅 fail 决定)。
|
|
@@ -3361,6 +3559,19 @@ export class FlowManager {
|
|
|
3361
3559
|
const raw = readFileSync(pipelinePath, 'utf-8');
|
|
3362
3560
|
const parsed = parseYaml(raw);
|
|
3363
3561
|
this.pipelineConfig = PipelineConfigSchema.parse(parsed);
|
|
3562
|
+
// 可选快线表:独立容错解析(非法类型 → 忽略该键 + 告警,不 fail 整体配置;缺失/空对象 → 内置快线表)
|
|
3563
|
+
const fastRaw = parsed?.['fast_transitions'];
|
|
3564
|
+
if (fastRaw !== undefined) {
|
|
3565
|
+
const fastParsed = FastTransitionsSchema.safeParse(fastRaw);
|
|
3566
|
+
if (fastParsed.success) {
|
|
3567
|
+
// [REV-74-009 修订] 空对象 {} 等同缺失(空表使 fast 全链不可推进,无合法用途)→ 回落内置快线表
|
|
3568
|
+
this.fastTransitions = Object.keys(fastParsed.data).length > 0 ? fastParsed.data : null;
|
|
3569
|
+
}
|
|
3570
|
+
else {
|
|
3571
|
+
this.fastTransitions = null;
|
|
3572
|
+
console.warn(t('flow.manager.fastTransitionsInvalid', getCliLang(this.projectPath)));
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3364
3575
|
}
|
|
3365
3576
|
catch {
|
|
3366
3577
|
// 解析失败时回退到内置默认值
|
|
@@ -3369,9 +3580,11 @@ export class FlowManager {
|
|
|
3369
3580
|
}
|
|
3370
3581
|
/**
|
|
3371
3582
|
* 获取内置默认流水线配置(等价于硬编码常量 + 3 个 Bug 修复)
|
|
3372
|
-
* 当 pipeline.yaml
|
|
3583
|
+
* 当 pipeline.yaml 不存在或解析失败时使用。
|
|
3584
|
+
* @param mode 工作流模式;**无参 = full**(内置 full 表逐字等于历史现状,零回归既有消费点);
|
|
3585
|
+
* 'fast' 返回快线表(FAST_TRANSITIONS 常量引用)。phases / checkpoint_mapping / phase_corrections 两模式共用。
|
|
3373
3586
|
*/
|
|
3374
|
-
getDefaultPipelineConfig() {
|
|
3587
|
+
getDefaultPipelineConfig(mode = 'full') {
|
|
3375
3588
|
return {
|
|
3376
3589
|
phases: [
|
|
3377
3590
|
'plan_pending', 'plan_review', 'plan_passed',
|
|
@@ -3380,25 +3593,27 @@ export class FlowManager {
|
|
|
3380
3593
|
'review_passed', 'test_pending', 'test_failed',
|
|
3381
3594
|
'test_passed', 'archiving', 'done',
|
|
3382
3595
|
],
|
|
3383
|
-
transitions:
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3596
|
+
transitions: mode === 'fast'
|
|
3597
|
+
? FAST_TRANSITIONS
|
|
3598
|
+
: {
|
|
3599
|
+
plan_pending: ['plan_review', 'plan_passed'],
|
|
3600
|
+
plan_review: ['plan_passed', 'plan_pending'],
|
|
3601
|
+
plan_passed: ['scheme_pending'],
|
|
3602
|
+
scheme_pending: ['scheme_review', 'scheme_passed'],
|
|
3603
|
+
scheme_review: ['scheme_passed', 'scheme_pending'],
|
|
3604
|
+
scheme_passed: ['exec_running'],
|
|
3605
|
+
exec_running: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
|
|
3606
|
+
review_pending: ['review_failed', 'review_passed'],
|
|
3607
|
+
review_failed: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
|
|
3608
|
+
review_passed: ['test_pending'],
|
|
3609
|
+
test_pending: ['test_failed', 'test_passed'],
|
|
3610
|
+
test_failed: ['test_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
|
|
3611
|
+
test_passed: ['archiving'],
|
|
3612
|
+
// 组合条件示例:审查通过 或 测试通过 任一即可推进到 archiving(多 Agent 并行场景,任一完成即推进)
|
|
3613
|
+
'review_passed|test_passed': ['archiving'],
|
|
3614
|
+
archiving: ['done'],
|
|
3615
|
+
done: [],
|
|
3616
|
+
},
|
|
3402
3617
|
checkpoint_mapping: {
|
|
3403
3618
|
plan: 'plan',
|
|
3404
3619
|
scheme: 'scheme',
|