openfeel 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.en.md +27 -8
  2. package/README.md +24 -5
  3. package/README.zh-CN.md +27 -8
  4. package/bin/openfeel.js +3 -2
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +5 -4
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/commands/config.d.ts.map +1 -1
  9. package/dist/commands/config.js +6 -0
  10. package/dist/commands/config.js.map +1 -1
  11. package/dist/commands/flow.d.ts.map +1 -1
  12. package/dist/commands/flow.js +106 -8
  13. package/dist/commands/flow.js.map +1 -1
  14. package/dist/commands/plan.d.ts.map +1 -1
  15. package/dist/commands/plan.js +7 -1
  16. package/dist/commands/plan.js.map +1 -1
  17. package/dist/commands/req.d.ts +2 -1
  18. package/dist/commands/req.d.ts.map +1 -1
  19. package/dist/commands/req.js +199 -26
  20. package/dist/commands/req.js.map +1 -1
  21. package/dist/commands/stage.d.ts.map +1 -1
  22. package/dist/commands/stage.js +7 -1
  23. package/dist/commands/stage.js.map +1 -1
  24. package/dist/core/config.d.ts +6 -4
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js +6 -5
  27. package/dist/core/config.js.map +1 -1
  28. package/dist/core/flow-manager.d.ts +60 -9
  29. package/dist/core/flow-manager.d.ts.map +1 -1
  30. package/dist/core/flow-manager.js +189 -38
  31. package/dist/core/flow-manager.js.map +1 -1
  32. package/dist/core/i18n-data/en.d.ts.map +1 -1
  33. package/dist/core/i18n-data/en.js +44 -8
  34. package/dist/core/i18n-data/en.js.map +1 -1
  35. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  36. package/dist/core/i18n-data/zh-CN.js +44 -8
  37. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  38. package/dist/core/managed-region.d.ts +16 -2
  39. package/dist/core/managed-region.d.ts.map +1 -1
  40. package/dist/core/managed-region.js +24 -3
  41. package/dist/core/managed-region.js.map +1 -1
  42. package/dist/core/metrics.d.ts +2 -0
  43. package/dist/core/metrics.d.ts.map +1 -1
  44. package/dist/core/metrics.js +15 -0
  45. package/dist/core/metrics.js.map +1 -1
  46. package/dist/core/model-config.d.ts +1 -1
  47. package/dist/core/model-config.d.ts.map +1 -1
  48. package/dist/core/model-config.js +2 -2
  49. package/dist/core/model-config.js.map +1 -1
  50. package/dist/core/opencode-config.js +1 -1
  51. package/dist/core/opencode-config.js.map +1 -1
  52. package/dist/core/pipeline-schema.d.ts +15 -0
  53. package/dist/core/pipeline-schema.d.ts.map +1 -1
  54. package/dist/core/pipeline-schema.js +17 -0
  55. package/dist/core/pipeline-schema.js.map +1 -1
  56. package/dist/core/plan/stage.d.ts +2 -1
  57. package/dist/core/plan/stage.d.ts.map +1 -1
  58. package/dist/core/plan/stage.js +7 -2
  59. package/dist/core/plan/stage.js.map +1 -1
  60. package/dist/core/req/aggregate.d.ts +2 -1
  61. package/dist/core/req/aggregate.d.ts.map +1 -1
  62. package/dist/core/req/aggregate.js.map +1 -1
  63. package/dist/core/req/body-file.d.ts.map +1 -1
  64. package/dist/core/req/body-file.js +7 -3
  65. package/dist/core/req/body-file.js.map +1 -1
  66. package/dist/core/req/requirement-ops.d.ts +44 -2
  67. package/dist/core/req/requirement-ops.d.ts.map +1 -1
  68. package/dist/core/req/requirement-ops.js +108 -3
  69. package/dist/core/req/requirement-ops.js.map +1 -1
  70. package/dist/core/req/types.d.ts +7 -1
  71. package/dist/core/req/types.d.ts.map +1 -1
  72. package/dist/core/req/types.js +0 -6
  73. package/dist/core/req/types.js.map +1 -1
  74. package/dist/core/runtime-log.d.ts +18 -0
  75. package/dist/core/runtime-log.d.ts.map +1 -1
  76. package/dist/core/runtime-log.js +72 -0
  77. package/dist/core/runtime-log.js.map +1 -1
  78. package/dist/core/session.d.ts +24 -0
  79. package/dist/core/session.d.ts.map +1 -0
  80. package/dist/core/session.js +107 -0
  81. package/dist/core/session.js.map +1 -0
  82. package/dist/core/template-loader.d.ts.map +1 -1
  83. package/dist/core/template-loader.js +2982 -6686
  84. package/dist/core/template-loader.js.map +1 -1
  85. package/dist/core/update.d.ts.map +1 -1
  86. package/dist/core/update.js +694 -33
  87. package/dist/core/update.js.map +1 -1
  88. package/package.json +1 -1
@@ -13,15 +13,16 @@ 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, FastTransitionsSchema, } from './pipeline-schema.js';
16
+ import { PipelineConfigSchema, PipelinePhaseSchema, PIPELINE_PHASES, MetaPhaseSchema, META_PHASES, transitionKeyMatches, parseTransitionKey, FastTransitionsSchema, TIER_PRUNED_PHASES, } 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, isArchivedVersionOf } from './plan/path.js';
20
20
  import { atomicWriteFileSync } from './fs/atomic-write.js';
21
21
  import { withFileLock, projectLockPath } from './fs/file-lock.js';
22
- import { DEFAULT_CONFIG, ConfigDefaultsSchema, STAGE_FIELD_BY_CONFIG_KEY } from './config.js';
22
+ import { DEFAULT_CONFIG, ConfigDefaultsSchema, STAGE_FIELD_BY_CONFIG_KEY, WORKFLOW_MODES } from './config.js';
23
23
  import { getGlobalProfilePath } from './global-paths.js';
24
24
  import { syncRequirementStatus } from './req/aggregate.js'; // 需求聚合回写入口(stage-83 T2;纯内存)
25
+ import { getSessionId } from './session.js'; // 会话标识(stage-86 T4;core→core 零环)
25
26
  /** flow.json 乐观并发冲突:磁盘 revision 与本次加载时不一致(存在并发写入) */
26
27
  export class FlowConcurrentModificationError extends Error {
27
28
  /** 本次实例加载时的 revision */
@@ -181,6 +182,17 @@ const FAST_TRANSITIONS = {
181
182
  archiving: ['done'],
182
183
  done: [],
183
184
  };
185
+ /**
186
+ * 工作流模式 → 派生的审查档位(**单源常量**;stage-92 D-92-3)。
187
+ * 仅用于「展示 / 提示」(`flow phases` 档位分节行与 `--json.reviewTier`);**不参与推进语义**
188
+ * (`flow advance` 的有效档位 = 显式声明 ?? full,缺省恒零裁剪 ⟹ 既有流程零回归)。
189
+ * general(无流水线)恒全量语义,不特判。
190
+ */
191
+ export const MODE_DERIVED_TIER = {
192
+ full: 'full',
193
+ fast: 'lean',
194
+ general: 'full',
195
+ };
184
196
  // ── 核心类 ──
185
197
  export class FlowManager {
186
198
  projectPath;
@@ -1050,14 +1062,48 @@ export class FlowManager {
1050
1062
  }
1051
1063
  return synced;
1052
1064
  }
1065
+ /**
1066
+ * T4:跨会话恢复判定。从 log 尾部回扫(本次条目尚未写入)取该 stage 最近一条
1067
+ * advance_stage_phase / reset_stage_phase 条目(要求 detail.to === exitedPhase);
1068
+ * 任一侧不可信 / 缺失 → 返回 null(调用方省略 resumed 字段,不臆断;proc: 级不参与跨会话判定)。
1069
+ * @param log 审计日志(本次条目尚未 append)
1070
+ * @param stageKey 阶段键(完整 stageId)
1071
+ * @param exitedPhase 本次转换退出的 phase(= 上一次转换进入的 phase)
1072
+ * @param currentId 本次会话标识(含来源前缀)
1073
+ * @returns true = 异会话恢复;false = 同会话;null = 不可判定(省略字段)
1074
+ */
1075
+ computeResumed(log, stageKey, exitedPhase, currentId) {
1076
+ // 可信判定:仅 env: / project: 前缀参与跨会话判定;proc: → 不可判定
1077
+ if (!(currentId.startsWith('env:') || currentId.startsWith('project:'))) {
1078
+ return null;
1079
+ }
1080
+ for (let k = log.length - 1; k >= 0; k--) {
1081
+ const e = log[k];
1082
+ if (e.action !== 'advance_stage_phase' && e.action !== 'reset_stage_phase') {
1083
+ continue;
1084
+ }
1085
+ const detail = e.detail;
1086
+ if (detail.stageName !== stageKey || detail.to !== exitedPhase) {
1087
+ continue;
1088
+ }
1089
+ const prevId = typeof detail.session_id === 'string' ? detail.session_id : '';
1090
+ // 旧条目无 session_id / 值非法 / 上一侧不可信 → 不可判定
1091
+ if (!(prevId.startsWith('env:') || prevId.startsWith('project:'))) {
1092
+ return null;
1093
+ }
1094
+ return prevId !== currentId; // 同值 → 同会话(false);异值 → 跨会话恢复(true)
1095
+ }
1096
+ return null; // 无历史转换条目 → 不可判定
1097
+ }
1053
1098
  /**
1054
1099
  * 推进指定阶段到目标流水线阶段(新 API)
1055
1100
  * @param stageName 阶段名(如 stage-01)
1056
1101
  * @param phase 目标流水线阶段(PipelinePhase)
1057
1102
  * @param triggeredBy 触发者标识(如 'cli' / 'Feel'),默认 'flow-manager'
1103
+ * @param opts.reviewTier 本次调用的审查档位(**仅显式声明时写审计键** `review_tier`;缺省不写 ⟹ 既有条目逐字不变)
1058
1104
  * @returns 是否触发 done 归档(true = 本次推进将阶段变为 done 且之前非 done,需在 save 后执行归档 commit)
1059
1105
  */
1060
- advanceStagePhase(stageName, phase, triggeredBy) {
1106
+ advanceStagePhase(stageName, phase, triggeredBy, opts) {
1061
1107
  if (!this.data) {
1062
1108
  return false;
1063
1109
  }
@@ -1123,12 +1169,21 @@ export class FlowManager {
1123
1169
  const allDone = Object.keys(this.data.stages).length > 0
1124
1170
  && Object.values(this.data.stages).every((s) => s.phase === 'done');
1125
1171
  this.data.pipeline.phase = (allDone ? 'done' : 'active');
1172
+ // T4:跨会话恢复标注(可选字段;任一侧不可信 / 缺失 → 省略 resumed,不臆断)
1173
+ // 计算在 appendLog 之前 → 回扫天然排除本次条目
1174
+ const sessionId = getSessionId({ projectPath: this.projectPath });
1175
+ const resumed = this.computeResumed(this.data.log, key, fromPhase, sessionId);
1126
1176
  // 追加日志(使用实际触发者名,而非硬编码 'flow-manager')
1177
+ // 审查档位留痕(stage-92 D-92-4):review_tier **仅显式声明时**追加(既有键逐字保留,缺省条目字节级不变)
1127
1178
  this.appendLog({
1128
1179
  time: '',
1129
1180
  agent: actualTrigger,
1130
1181
  action: 'advance_stage_phase',
1131
- detail: { stageName: key, from: fromPhase, to: targetPhase },
1182
+ detail: {
1183
+ stageName: key, from: fromPhase, to: targetPhase, session_id: sessionId,
1184
+ ...(resumed === null ? {} : { resumed }),
1185
+ ...(opts?.reviewTier === undefined ? {} : { review_tier: opts.reviewTier }),
1186
+ },
1132
1187
  });
1133
1188
  // REV-003: advance_stage_phase 改为 endStage 时批量聚合,取消逐条写入
1134
1189
  // this.publicLogger.logPhaseChange({...});
@@ -1204,11 +1259,14 @@ export class FlowManager {
1204
1259
  stage.phase = targetPhase;
1205
1260
  stage.status = projectedStatus;
1206
1261
  // 审计日志:reset 为修复而非里程碑推进
1262
+ // T4:跨会话恢复标注(与 advanceStagePhase 同款;计算在 appendLog 之前)
1263
+ const resetSessionId = getSessionId({ projectPath: this.projectPath });
1264
+ const resetResumed = this.computeResumed(this.data.log, key, fromPhase, resetSessionId);
1207
1265
  this.appendLog({
1208
1266
  time: '',
1209
1267
  agent: 'flow-manager',
1210
1268
  action: 'reset_stage_phase',
1211
- detail: { stageName: key, from: fromPhase, to: targetPhase },
1269
+ detail: { stageName: key, from: fromPhase, to: targetPhase, session_id: resetSessionId, ...(resetResumed === null ? {} : { resumed: resetResumed }) },
1212
1270
  });
1213
1271
  // 需求聚合对称回退(T2):聚合视图重算——聚合 ≠ done ∧ 需求 status === done → 回退 in_progress;
1214
1272
  // 纯内存;dryRun / no-op 已在 :1585 早退(本点不可达);不新增写盘点。
@@ -1504,10 +1562,11 @@ export class FlowManager {
1504
1562
  * 无 '|' 的简单 key 保持单条件原行为(精确相等)。
1505
1563
  * @param fromPhase 当前源 phase
1506
1564
  * @param mode 工作流模式(**必传**;由调用点经 resolveStageWorkflowMode 从 stage 派生——禁止默认 mode)
1565
+ * @param tier 审查档位(可选,缺省 'full' = 零裁剪 ⟹ 既有调用逐字零回归)
1507
1566
  * @returns 去重后的可达目标 phase 列表
1508
1567
  */
1509
- getValidTargets(fromPhase, mode) {
1510
- const transitions = this.resolveTransitionsForMode(mode);
1568
+ getValidTargets(fromPhase, mode, tier = 'full') {
1569
+ const transitions = this.resolveTransitionsForMode(mode, tier);
1511
1570
  const result = new Set();
1512
1571
  for (const [key, targets] of Object.entries(transitions)) {
1513
1572
  if (transitionKeyMatches(key, fromPhase)) {
@@ -1537,8 +1596,9 @@ export class FlowManager {
1537
1596
  * 获取当前阶段的所有可达下一阶段(基于 transitions 表,支持组合条件)
1538
1597
  * 供 flow wizard 交互模式使用
1539
1598
  * @param stageName 可选阶段名,提供时使用该阶段 phase 进行查找;否则使用 current.stage 的 phase
1599
+ * @param tier 审查档位(可选,缺省 'full' = 零裁剪)
1540
1600
  */
1541
- getAvailablePhases(stageName) {
1601
+ getAvailablePhases(stageName, tier = 'full') {
1542
1602
  if (!this.data) {
1543
1603
  return [];
1544
1604
  }
@@ -1549,7 +1609,7 @@ export class FlowManager {
1549
1609
  if (!currentPhase)
1550
1610
  return [];
1551
1611
  const mode = this.resolveStageWorkflowMode(stageName).value;
1552
- return this.getValidTargets(currentPhase, mode);
1612
+ return this.getValidTargets(currentPhase, mode, tier);
1553
1613
  }
1554
1614
  /**
1555
1615
  * 在运行时 transitions 上求 stageName 当前 phase → to 的**唯一**可达路径(B5-1)。
@@ -1557,9 +1617,10 @@ export class FlowManager {
1557
1617
  * 纯只读:不写盘、不改内存。
1558
1618
  * @param stageName 阶段 ID
1559
1619
  * @param to 目标 phase
1620
+ * @param tier 审查档位(可选,缺省 'full' = 零裁剪;显式声明时按裁剪表求路径)
1560
1621
  * @returns 路径与原因(no-path/ambiguous/depth-exceeded 供命令层报错;already-at-target 按 no-op)
1561
1622
  */
1562
- findPhasePath(stageName, to) {
1623
+ findPhasePath(stageName, to, tier = 'full') {
1563
1624
  // REV-005:归一化 stageId(短名 → 全名),与 setStageDeps / plan scheme * 同范式
1564
1625
  // 双键回退:兼容以短名建键的存量 / 测试数据(归一化键不存在时回退原始键)
1565
1626
  const normalized = normalizeStageId(stageName) ?? stageName;
@@ -1585,7 +1646,7 @@ export class FlowManager {
1585
1646
  if (node.path.length >= PHASE_PATH_MAX_DEPTH) {
1586
1647
  continue;
1587
1648
  }
1588
- for (const target of this.getValidTargets(node.phase, mode)) {
1649
+ for (const target of this.getValidTargets(node.phase, mode, tier)) {
1589
1650
  const path = [...node.path, target];
1590
1651
  if (target === to) {
1591
1652
  hits.push(path);
@@ -1622,24 +1683,85 @@ export class FlowManager {
1622
1683
  const phases = this.pipelineConfig?.phases ?? this.getDefaultPipelineConfig().phases;
1623
1684
  return [...phases];
1624
1685
  }
1686
+ /**
1687
+ * ═══ 审查档位裁剪(stage-92 D-92-2 / D-92-3;**唯一调用点** = resolveTransitionsForMode)═══
1688
+ * 纯函数:不改入参(不污染 FAST_TRANSITIONS 常量 / pipelineConfig),恒返回新对象 + 新数组。
1689
+ * 算法:
1690
+ * ① `tier === 'full'` → 深拷贝原样返回(零裁剪 ⟹ 既有调用逐字零回归);
1691
+ * ② 各 key 的 targets 过滤被裁剪相位(目标侧裁剪 = 跳过该审查关);
1692
+ * ③ **边收缩回退**——若某 key 过滤后 targets 为空且原 targets 非空,则并入「被删目标的 targets」
1693
+ * (再过滤被裁剪项与自身)后去重。该回退保证 fast 基表裁 `plan_review` 后 `plan_pending` 非空集
1694
+ * (`['plan_review']` → 收缩 `plan_review` 的 targets → `plan_pending → ['plan_passed']`)。
1695
+ * **裁剪只作用于入边 / 目标侧**:被裁剪相位保留出边(阶段已处于被裁剪关时可向该档位下游推进);
1696
+ * 「**进入**被裁剪关」仍被目标侧过滤裁掉。
1697
+ * 边界(登记):含被裁剪相位的**组合 key**(如 `'scheme_review|x'`)按 ② 过滤 targets(不再整键删除);
1698
+ * 内置 full/fast 表与本仓运行时表均无此类键(唯一组合键 `'review_passed|test_passed'` 不含被裁剪相位)。
1699
+ * @param table 模式基表(由 resolveTransitionsForMode 按既有三分支解析所得)
1700
+ * @param tier 审查档位
1701
+ * @returns 裁剪后的新表
1702
+ */
1703
+ pruneTransitionsByTier(table, tier) {
1704
+ // 全量档:零裁剪(深拷贝返回,防调用方改内存污染基表)
1705
+ if (tier === 'full') {
1706
+ return Object.fromEntries(Object.entries(table).map(([k, v]) => [k, [...v]]));
1707
+ }
1708
+ const pruned = TIER_PRUNED_PHASES[tier];
1709
+ const isPruned = (p) => pruned.includes(p);
1710
+ const result = {};
1711
+ for (const [key, targets] of Object.entries(table)) {
1712
+ const keyConds = parseTransitionKey(key);
1713
+ // ② 目标侧过滤被裁剪相位(裁剪只作用于入边 / 目标侧——被裁剪相位作源键时保留,其出边可推进)
1714
+ let filtered = targets.filter((t) => !isPruned(t));
1715
+ // ③ 边收缩回退:过滤后为空且原 targets 非空 → 并入被删目标的 targets(过滤被裁剪项与自身后去重)
1716
+ if (filtered.length === 0 && targets.length > 0) {
1717
+ const merged = new Set();
1718
+ for (const dropped of targets) {
1719
+ if (!isPruned(dropped)) {
1720
+ continue;
1721
+ }
1722
+ for (const cascaded of table[dropped] ?? []) {
1723
+ if (!isPruned(cascaded) && !keyConds.includes(cascaded)) {
1724
+ merged.add(cascaded);
1725
+ }
1726
+ }
1727
+ }
1728
+ filtered = [...merged];
1729
+ }
1730
+ result[key] = filtered;
1731
+ }
1732
+ return result;
1733
+ }
1625
1734
  /**
1626
1735
  * 按模式解析运行时转移表(唯一取表法则;校验/推进/路径 与 展示 同经本函数 ⇒ 单一信源)。
1627
1736
  * full → 运行时 pipelineConfig.transitions(pipeline.yaml 覆盖线);fast → fast_transitions ?? 内置快线表。
1737
+ * general → 空表(无流水线;模式语义本身,非「缺省回落」)。
1738
+ * 审查档位为**第二参延伸**(stage-92 D-92-3):末尾统一经 pruneTransitionsByTier 裁剪;
1739
+ * 缺省 tier='full' = 零裁剪 ⟹ 与原逻辑等价(唯一取表点不变,禁第二份层序 / 取表)。
1628
1740
  * 说明:pipelineConfig 构造期恒已加载(constructor 无条件调用),`?? 内置` 保留既有访问器回退语义。
1629
1741
  */
1630
- resolveTransitionsForMode(mode) {
1742
+ resolveTransitionsForMode(mode, tier = 'full') {
1743
+ let raw;
1631
1744
  if (mode === 'fast') {
1632
- return this.fastTransitions ?? this.getDefaultPipelineConfig('fast').transitions;
1745
+ raw = this.fastTransitions ?? this.getDefaultPipelineConfig('fast').transitions;
1746
+ }
1747
+ else if (mode === 'general') {
1748
+ // general(无流水线):空表为**模式语义**的忠实表达(D-90-3;与 pipeline.yaml fast_transitions:{} 的「配置层空值回落」路径 / 语义均不同)
1749
+ raw = {};
1633
1750
  }
1634
- return this.pipelineConfig?.transitions ?? this.getDefaultPipelineConfig().transitions;
1751
+ else {
1752
+ raw = this.pipelineConfig?.transitions ?? this.getDefaultPipelineConfig().transitions;
1753
+ }
1754
+ // 档位裁剪统一落此处(唯一取表点的单一裁剪入口;general 空表裁剪后仍空表)
1755
+ return this.pruneTransitionsByTier(raw, tier);
1635
1756
  }
1636
1757
  /**
1637
1758
  * 获取运行时生效的 phase 转移表(key 可含 '|' 组合条件)。
1638
1759
  * 缺省/解析失败时回退内置默认配置。返回浅拷贝。
1639
1760
  * **无参 = full**(零回归既有消费点);fast 供 stage-75 `flow phases` 模式分节消费。
1761
+ * @param tier 审查档位(可选,缺省 'full' = 零裁剪)
1640
1762
  */
1641
- getPipelineTransitions(mode = 'full') {
1642
- const transitions = this.resolveTransitionsForMode(mode);
1763
+ getPipelineTransitions(mode = 'full', tier = 'full') {
1764
+ const transitions = this.resolveTransitionsForMode(mode, tier);
1643
1765
  return Object.fromEntries(Object.entries(transitions).map(([k, v]) => [k, [...v]]));
1644
1766
  }
1645
1767
  /**
@@ -1809,7 +1931,8 @@ export class FlowManager {
1809
1931
  // 匹配执行模式、自动推进、工作流模式
1810
1932
  const execMatch = content.match(/\*\*执行模式\*\*[::]\s*(manual|auto)/);
1811
1933
  const autoMatch = content.match(/\*\*自动推进\*\*[::]\s*(disabled|enabled)/);
1812
- const modeMatch = content.match(/\*\*工作流模式\*\*[::]\s*(full|fast)/);
1934
+ // 值域自 WORKFLOW_MODES 派生(单源;stage-90——漏改本行即第 1 层丢弃、静默回落 config)
1935
+ const modeMatch = content.match(new RegExp(`\\*\\*工作流模式\\*\\*[::]\\s*(${WORKFLOW_MODES.join('|')})`));
1813
1936
  if (execMatch) {
1814
1937
  statusOverrides['execution_mode'] = execMatch[1];
1815
1938
  }
@@ -1839,6 +1962,11 @@ export class FlowManager {
1839
1962
  const cascade = this.buildCascadeConfig();
1840
1963
  const result = {};
1841
1964
  for (const key of EFFECTIVE_CONFIG_KEYS) {
1965
+ if (key === 'workflow_mode') {
1966
+ // Q4 定稿(D-90-6):委托单点层级函数(无参 = 当前阶段域,与 config effective 语义域一致);禁第二份层序判定
1967
+ result[key] = this.resolveStageWorkflowMode();
1968
+ continue;
1969
+ }
1842
1970
  if (cascade.statusOverrides[key] !== undefined) {
1843
1971
  result[key] = { value: cascade.statusOverrides[key], source: 'status.md' };
1844
1972
  }
@@ -1858,11 +1986,11 @@ export class FlowManager {
1858
1986
  /**
1859
1987
  * 解析指定阶段的工作流模式(裁定 E:「stage → mode」表选择解析入口;stage-75 `flow mode` 契约)。
1860
1988
  * 带 stage 形参独立解析(REV-74-001):该阶段 status.md(findStatusPath(stageId))
1861
- * > config.yaml defaults > profile.yaml(**有意不承载**,实际不产出)> builtin(DEFAULT_CONFIG.workflow_mode = fast)。
1989
+ * > 需求级(关联需求携带的 workflow_mode)> config.yaml defaults > profile.yaml(**有意不承载**,实际不产出)> builtin(DEFAULT_CONFIG.workflow_mode = fast)。
1862
1990
  * 语义边界:`config effective` / `flow status --verbose` = **当前阶段域**(无参);
1863
1991
  * 本方法显式 stageId = **任意阶段域**(flow mode --stage / 表选择)。
1864
1992
  * @param stageId 目标阶段 ID(缺省 = pipeline.current.stage;无该阶段 status.md → 自然回落下一层)
1865
- * @returns { value, source },source ∈ status.md | config.yaml | profile.yaml | builtin
1993
+ * @returns { value, source },source ∈ status.md | requirement | config.yaml | profile.yaml | builtin
1866
1994
  */
1867
1995
  resolveStageWorkflowMode(stageId) {
1868
1996
  const cascade = this.buildCascadeConfig(stageId);
@@ -1870,6 +1998,18 @@ export class FlowManager {
1870
1998
  if (cascade.statusOverrides[key] !== undefined) {
1871
1999
  return { value: cascade.statusOverrides[key], source: 'status.md' };
1872
2000
  }
2001
+ // ── 需求级层(stage-90 T2.2):阶段关联需求携带的模式属性 ──
2002
+ // 读侧宽容:缺失 / 悬空 / 非法 → 自然回落下一层(零报错);阶段定位与 status.md 层同域(归一 + 键回退)
2003
+ const reqTarget = stageId ?? this.data?.pipeline.current.stage;
2004
+ const reqStageRec = reqTarget ? this.data?.stages?.[normalizeStageId(reqTarget) ?? reqTarget] : undefined;
2005
+ const reqId = reqStageRec?.requirement_id ?? null;
2006
+ if (reqId) {
2007
+ const reqRec = (Array.isArray(this.data?.requirements) ? this.data.requirements : []).find((r) => r?.id === reqId);
2008
+ const reqMode = reqRec?.workflow_mode;
2009
+ if (reqMode !== undefined && WORKFLOW_MODES.includes(reqMode)) {
2010
+ return { value: reqMode, source: 'requirement' };
2011
+ }
2012
+ }
1873
2013
  if (cascade.configDefaults[key] !== undefined) {
1874
2014
  return { value: cascade.configDefaults[key], source: 'config.yaml' };
1875
2015
  }
@@ -2005,6 +2145,9 @@ export class FlowManager {
2005
2145
  const stageIdOfOp = this.parseOpId(opId)?.stageId;
2006
2146
  if (result === 'pass') {
2007
2147
  op.state = 'done';
2148
+ // T3:op 级埋点(唯一写入点 = recordAttempt;pass 同时进入 done 终态)
2149
+ op.attempted_at = new Date().toISOString();
2150
+ op.completed_at = op.attempted_at;
2008
2151
  if (stageIdOfOp) {
2009
2152
  this.syncCurrentOp(stageIdOfOp);
2010
2153
  }
@@ -2026,6 +2169,8 @@ export class FlowManager {
2026
2169
  // result === 'fail'
2027
2170
  if (op.attempts < op.max_attempts) {
2028
2171
  op.state = 'pending'; // 回到 pending 等待重试
2172
+ // T3:仅记 attempted_at(未进入终态 → completed_at 缺省)
2173
+ op.attempted_at = new Date().toISOString();
2029
2174
  if (stageIdOfOp) {
2030
2175
  this.syncCurrentOp(stageIdOfOp);
2031
2176
  }
@@ -2046,6 +2191,9 @@ export class FlowManager {
2046
2191
  }
2047
2192
  // 重试耗尽
2048
2193
  op.state = 'failed';
2194
+ // T3:重试耗尽 = 进入 failed 终态 → 双写
2195
+ op.attempted_at = new Date().toISOString();
2196
+ op.completed_at = op.attempted_at;
2049
2197
  this.data.pipeline.retry += 1;
2050
2198
  this.appendLog({
2051
2199
  time: '',
@@ -3624,6 +3772,7 @@ export class FlowManager {
3624
3772
  * 当 pipeline.yaml 不存在或解析失败时使用。
3625
3773
  * @param mode 工作流模式;**无参 = full**(内置 full 表逐字等于历史现状,零回归既有消费点);
3626
3774
  * 'fast' 返回快线表(FAST_TRANSITIONS 常量引用)。phases / checkpoint_mapping / phase_corrections 两模式共用。
3775
+ * 'general' 返回空转移表(无流水线;phases 仍 15 项)。
3627
3776
  */
3628
3777
  getDefaultPipelineConfig(mode = 'full') {
3629
3778
  return {
@@ -3636,25 +3785,27 @@ export class FlowManager {
3636
3785
  ],
3637
3786
  transitions: mode === 'fast'
3638
3787
  ? FAST_TRANSITIONS
3639
- : {
3640
- plan_pending: ['plan_review', 'plan_passed'],
3641
- plan_review: ['plan_passed', 'plan_pending'],
3642
- plan_passed: ['scheme_pending'],
3643
- scheme_pending: ['scheme_review', 'scheme_passed'],
3644
- scheme_review: ['scheme_passed', 'scheme_pending'],
3645
- scheme_passed: ['exec_running'],
3646
- exec_running: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3647
- review_pending: ['review_failed', 'review_passed'],
3648
- review_failed: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3649
- review_passed: ['test_pending'],
3650
- test_pending: ['test_failed', 'test_passed'],
3651
- test_failed: ['test_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3652
- test_passed: ['archiving'],
3653
- // 组合条件示例:审查通过 或 测试通过 任一即可推进到 archiving(多 Agent 并行场景,任一完成即推进)
3654
- 'review_passed|test_passed': ['archiving'],
3655
- archiving: ['done'],
3656
- done: [],
3657
- },
3788
+ : mode === 'general'
3789
+ ? {} // general:无流水线(空表;phases 仍 15 项,不新增 phase)
3790
+ : {
3791
+ plan_pending: ['plan_review', 'plan_passed'],
3792
+ plan_review: ['plan_passed', 'plan_pending'],
3793
+ plan_passed: ['scheme_pending'],
3794
+ scheme_pending: ['scheme_review', 'scheme_passed'],
3795
+ scheme_review: ['scheme_passed', 'scheme_pending'],
3796
+ scheme_passed: ['exec_running'],
3797
+ exec_running: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3798
+ review_pending: ['review_failed', 'review_passed'],
3799
+ review_failed: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3800
+ review_passed: ['test_pending'],
3801
+ test_pending: ['test_failed', 'test_passed'],
3802
+ test_failed: ['test_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3803
+ test_passed: ['archiving'],
3804
+ // 组合条件示例:审查通过 或 测试通过 任一即可推进到 archiving(多 Agent 并行场景,任一完成即推进)
3805
+ 'review_passed|test_passed': ['archiving'],
3806
+ archiving: ['done'],
3807
+ done: [],
3808
+ },
3658
3809
  checkpoint_mapping: {
3659
3810
  plan: 'plan',
3660
3811
  scheme: 'scheme',