openfeel 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.en.md +11 -11
  2. package/README.md +2 -1
  3. package/README.zh-CN.md +13 -13
  4. package/bin/openfeel.js +2 -2
  5. package/dist/cli/index.d.ts +6 -0
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +26 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/commands/archive.d.ts +1 -1
  10. package/dist/commands/flow.d.ts +1 -1
  11. package/dist/commands/flow.d.ts.map +1 -1
  12. package/dist/commands/flow.js +20 -8
  13. package/dist/commands/flow.js.map +1 -1
  14. package/dist/commands/migrate.d.ts +10 -0
  15. package/dist/commands/migrate.d.ts.map +1 -0
  16. package/dist/commands/migrate.js +97 -0
  17. package/dist/commands/migrate.js.map +1 -0
  18. package/dist/commands/model.d.ts +10 -0
  19. package/dist/commands/model.d.ts.map +1 -0
  20. package/dist/commands/model.js +135 -0
  21. package/dist/commands/model.js.map +1 -0
  22. package/dist/commands/plan.js +2 -2
  23. package/dist/commands/plan.js.map +1 -1
  24. package/dist/commands/stage.d.ts.map +1 -1
  25. package/dist/commands/stage.js +66 -47
  26. package/dist/commands/stage.js.map +1 -1
  27. package/dist/commands/update.d.ts.map +1 -1
  28. package/dist/commands/update.js +12 -0
  29. package/dist/commands/update.js.map +1 -1
  30. package/dist/commands/view.d.ts +1 -1
  31. package/dist/commands/view.d.ts.map +1 -1
  32. package/dist/commands/view.js +2 -1
  33. package/dist/commands/view.js.map +1 -1
  34. package/dist/core/archive/merge.d.ts.map +1 -1
  35. package/dist/core/archive/merge.js +5 -4
  36. package/dist/core/archive/merge.js.map +1 -1
  37. package/dist/core/artifact-graph/resolver.js +2 -2
  38. package/dist/core/artifact-graph/resolver.js.map +1 -1
  39. package/dist/core/config.d.ts +8 -8
  40. package/dist/core/config.d.ts.map +1 -1
  41. package/dist/core/config.js +22 -17
  42. package/dist/core/config.js.map +1 -1
  43. package/dist/core/flow-manager.d.ts +27 -6
  44. package/dist/core/flow-manager.d.ts.map +1 -1
  45. package/dist/core/flow-manager.js +152 -103
  46. package/dist/core/flow-manager.js.map +1 -1
  47. package/dist/core/fs/atomic-write.d.ts +31 -0
  48. package/dist/core/fs/atomic-write.d.ts.map +1 -0
  49. package/dist/core/fs/atomic-write.js +80 -0
  50. package/dist/core/fs/atomic-write.js.map +1 -0
  51. package/dist/core/fs/file-lock.d.ts +34 -0
  52. package/dist/core/fs/file-lock.d.ts.map +1 -0
  53. package/dist/core/fs/file-lock.js +142 -0
  54. package/dist/core/fs/file-lock.js.map +1 -0
  55. package/dist/core/fs/sequence.d.ts +42 -0
  56. package/dist/core/fs/sequence.d.ts.map +1 -0
  57. package/dist/core/fs/sequence.js +69 -0
  58. package/dist/core/fs/sequence.js.map +1 -0
  59. package/dist/core/global-paths.d.ts +25 -0
  60. package/dist/core/global-paths.d.ts.map +1 -0
  61. package/dist/core/global-paths.js +57 -0
  62. package/dist/core/global-paths.js.map +1 -0
  63. package/dist/core/i18n-data/en.d.ts +2 -0
  64. package/dist/core/i18n-data/en.d.ts.map +1 -1
  65. package/dist/core/i18n-data/en.js +69 -1
  66. package/dist/core/i18n-data/en.js.map +1 -1
  67. package/dist/core/i18n-data/zh-CN.d.ts +2 -0
  68. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  69. package/dist/core/i18n-data/zh-CN.js +69 -1
  70. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  71. package/dist/core/i18n.js +4 -4
  72. package/dist/core/i18n.js.map +1 -1
  73. package/dist/core/init.d.ts +3 -3
  74. package/dist/core/init.d.ts.map +1 -1
  75. package/dist/core/init.js +58 -42
  76. package/dist/core/init.js.map +1 -1
  77. package/dist/core/managed-region.d.ts +54 -0
  78. package/dist/core/managed-region.d.ts.map +1 -0
  79. package/dist/core/managed-region.js +171 -0
  80. package/dist/core/managed-region.js.map +1 -0
  81. package/dist/core/metrics.d.ts.map +1 -1
  82. package/dist/core/metrics.js +6 -3
  83. package/dist/core/metrics.js.map +1 -1
  84. package/dist/core/migrate.d.ts +103 -0
  85. package/dist/core/migrate.d.ts.map +1 -0
  86. package/dist/core/migrate.js +533 -0
  87. package/dist/core/migrate.js.map +1 -0
  88. package/dist/core/model-config.d.ts +62 -0
  89. package/dist/core/model-config.d.ts.map +1 -0
  90. package/dist/core/model-config.js +336 -0
  91. package/dist/core/model-config.js.map +1 -0
  92. package/dist/core/opencode-config.d.ts +17 -0
  93. package/dist/core/opencode-config.d.ts.map +1 -0
  94. package/dist/core/opencode-config.js +123 -0
  95. package/dist/core/opencode-config.js.map +1 -0
  96. package/dist/core/plan/path.d.ts +64 -0
  97. package/dist/core/plan/path.d.ts.map +1 -0
  98. package/dist/core/plan/path.js +150 -0
  99. package/dist/core/plan/path.js.map +1 -0
  100. package/dist/core/plan/roadmap.d.ts.map +1 -1
  101. package/dist/core/plan/roadmap.js +3 -2
  102. package/dist/core/plan/roadmap.js.map +1 -1
  103. package/dist/core/plan/scheme.d.ts +4 -3
  104. package/dist/core/plan/scheme.d.ts.map +1 -1
  105. package/dist/core/plan/scheme.js +113 -110
  106. package/dist/core/plan/scheme.js.map +1 -1
  107. package/dist/core/plan/stage.d.ts +2 -2
  108. package/dist/core/plan/stage.d.ts.map +1 -1
  109. package/dist/core/plan/stage.js +47 -32
  110. package/dist/core/plan/stage.js.map +1 -1
  111. package/dist/core/public-logger.d.ts +3 -3
  112. package/dist/core/public-logger.d.ts.map +1 -1
  113. package/dist/core/public-logger.js +36 -40
  114. package/dist/core/public-logger.js.map +1 -1
  115. package/dist/core/template-loader.d.ts +1 -1
  116. package/dist/core/template-loader.d.ts.map +1 -1
  117. package/dist/core/template-loader.js +2849 -2826
  118. package/dist/core/template-loader.js.map +1 -1
  119. package/dist/core/update-infos.d.ts +32 -0
  120. package/dist/core/update-infos.d.ts.map +1 -0
  121. package/dist/core/update-infos.js +153 -0
  122. package/dist/core/update-infos.js.map +1 -0
  123. package/dist/core/update-state.d.ts +18 -0
  124. package/dist/core/update-state.d.ts.map +1 -1
  125. package/dist/core/update-state.js +66 -3
  126. package/dist/core/update-state.js.map +1 -1
  127. package/dist/core/update.d.ts +12 -0
  128. package/dist/core/update.d.ts.map +1 -1
  129. package/dist/core/update.js +340 -485
  130. package/dist/core/update.js.map +1 -1
  131. package/dist/core/view/entry.js +4 -4
  132. package/dist/core/view/entry.js.map +1 -1
  133. package/dist/core/workspace/identity.d.ts.map +1 -1
  134. package/dist/core/workspace/identity.js +13 -10
  135. package/dist/core/workspace/identity.js.map +1 -1
  136. package/dist/core/workspace/knowledge.d.ts.map +1 -1
  137. package/dist/core/workspace/knowledge.js +24 -14
  138. package/dist/core/workspace/knowledge.js.map +1 -1
  139. package/package.json +1 -1
@@ -0,0 +1,64 @@
1
+ /** 短名(无版本前缀)默认系列 */
2
+ export declare const DEFAULT_SERIES = "v1";
3
+ /** 短名规范化为完整 stageId 时的默认四级版本前缀(与 init 示例阶段一致) */
4
+ export declare const DEFAULT_STAGE_VERSION = "v1.0.0";
5
+ /** stageId 解析结果 */
6
+ export interface ParsedStageId {
7
+ /** 大版本系列,如 v1 */
8
+ series: string;
9
+ /** 阶段目录名,如 stage-34 */
10
+ stageDir: string;
11
+ /** 规范化后的完整 stageId(短名补齐版本前缀) */
12
+ fullStageId: string;
13
+ }
14
+ /**
15
+ * 解析 stageId → { series, stageDir, fullStageId }
16
+ *
17
+ * 支持三种格式(决策 2 正向映射):
18
+ * - 完整:v1.0.0-stage-34 → { series: 'v1', stageDir: 'stage-34', fullStageId: 'v1.0.0-stage-34' }
19
+ * - 历史:v4-stage-04 → { series: 'v4', stageDir: 'stage-04', fullStageId: 'v4-stage-04' }
20
+ * - 短名:stage-01 → { series: 'v1', stageDir: 'stage-01', fullStageId: 'v1.0.0-stage-01' }
21
+ *
22
+ * 无法解析(如 'foo'、'v1'、空字符串)返回 null。
23
+ * 注意 stageDir 保留原始数字格式(stage-04 不丢前导零),NN 用捕获组原样保留,不做 parseInt。
24
+ */
25
+ export declare function parseStageId(stageId: string): ParsedStageId | null;
26
+ /**
27
+ * stageId → plan 相对目录(不含 .openfeel 前缀)
28
+ * 例:v1.0.0-stage-34 → 'plan/v1/stage-34/';stage-01 → 'plan/v1/stage-01/'
29
+ * 无法解析返回 null。
30
+ */
31
+ export declare function stageIdToPlanDir(stageId: string): string | null;
32
+ /**
33
+ * 短名/完整 stageId → 规范化完整 stageId
34
+ * 短名 stage-01 → v1.0.0-stage-01;完整/历史格式原样返回。
35
+ * 无法解析返回 null。
36
+ */
37
+ export declare function normalizeStageId(stageId: string): string | null;
38
+ /**
39
+ * 目录名(stage-NN)→ 完整 stageId(反向映射,回查 flow.json stages 键)
40
+ *
41
+ * 目录名 stage-33 无法唯一还原版本前缀,须回查 flow.json 匹配 `*-stage-33`。
42
+ * 去歧义规则(遗留风险 1):若多个 stageId 匹配 `*-stage-{NN}`,
43
+ * 1. 优先 pipeline.current.stage(若在匹配集中);
44
+ * 2. 否则按版本前缀字典序降序取最新(v1.0.0 > v0.9 > v0.5)。
45
+ * 注:同 stage-NN 跨多版本前缀重复在理论上不应发生,此规则仅兜底。
46
+ *
47
+ * @returns 完整 stageId,找不到匹配或 flow.json 不可读时返回 null
48
+ */
49
+ export declare function planDirToStageId(projectPath: string, stageDir: string): string | null;
50
+ /**
51
+ * 三级回退查找 status.md(决策 4,供 flow-manager 与命令层复用)
52
+ *
53
+ * 1. plan/{series}/stage-NN/status.md(解析 stageId → 精确路径,首选)
54
+ * 2. plan/ 下递归搜索 stage-NN/status.md(fast-glob 递归,兼容 series 变化/旧平铺)
55
+ * 3. stages/{stageId}/status.md(历史遗留,只读兜底)
56
+ *
57
+ * 性能说明(遗留风险 3):第 1 级精确路径命中率最高(新写入均在此),
58
+ * 第 2 级 fast-glob 仅在精确未命中时触发(兼容旧数据/系列变化的罕见场景),
59
+ * 因此对 flow status 高频调用性能影响可控;本 stage 不做缓存,留作后续观察。
60
+ *
61
+ * @returns status.md 绝对路径,三级均未命中返回 null
62
+ */
63
+ export declare function findStageStatusPath(projectPath: string, stageId: string): string | null;
64
+ //# sourceMappingURL=path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../../src/core/plan/path.ts"],"names":[],"mappings":"AAaA,oBAAoB;AACpB,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC,iDAAiD;AACjD,eAAO,MAAM,qBAAqB,WAAW,CAAC;AAE9C,mBAAmB;AACnB,MAAM,WAAW,aAAa;IAC5B,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAsBlE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6BrF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA+BvF"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * 阶段路径映射工具 — stageId ↔ plan 目录双向映射的唯一权威
3
+ *
4
+ * 统一规则(决策 1/2):
5
+ * - plan 目录结构:.openfeel/plan/{series}/stage-{NN}/
6
+ * - {series} = 大版本系列 = v{MAJOR}(如 v1、v4)
7
+ * - {stage} = 阶段目录名 = stage-{NN}(不含版本前缀)
8
+ * - flow.json stageId 是权威标识(唯一键),目录名是派生组织单位
9
+ */
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
12
+ import fastGlob from 'fast-glob';
13
+ /** 短名(无版本前缀)默认系列 */
14
+ export const DEFAULT_SERIES = 'v1';
15
+ /** 短名规范化为完整 stageId 时的默认四级版本前缀(与 init 示例阶段一致) */
16
+ export const DEFAULT_STAGE_VERSION = 'v1.0.0';
17
+ /**
18
+ * 解析 stageId → { series, stageDir, fullStageId }
19
+ *
20
+ * 支持三种格式(决策 2 正向映射):
21
+ * - 完整:v1.0.0-stage-34 → { series: 'v1', stageDir: 'stage-34', fullStageId: 'v1.0.0-stage-34' }
22
+ * - 历史:v4-stage-04 → { series: 'v4', stageDir: 'stage-04', fullStageId: 'v4-stage-04' }
23
+ * - 短名:stage-01 → { series: 'v1', stageDir: 'stage-01', fullStageId: 'v1.0.0-stage-01' }
24
+ *
25
+ * 无法解析(如 'foo'、'v1'、空字符串)返回 null。
26
+ * 注意 stageDir 保留原始数字格式(stage-04 不丢前导零),NN 用捕获组原样保留,不做 parseInt。
27
+ */
28
+ export function parseStageId(stageId) {
29
+ // 完整/历史格式:{version}-stage-{NN},version 可为 v1.0.0(四级)或 v4(仅主版本)
30
+ const full = stageId.match(/^(v\d+(?:\.\d+)*)-stage-(\d+)$/);
31
+ if (full) {
32
+ const version = full[1]; // v1.0.0 或 v4
33
+ const stageDir = `stage-${full[2]}`;
34
+ const series = version.split('.')[0]; // v1 或 v4
35
+ return { series, stageDir, fullStageId: stageId };
36
+ }
37
+ // 短名格式:stage-{NN}
38
+ const short = stageId.match(/^stage-(\d+)$/);
39
+ if (short) {
40
+ const stageDir = `stage-${short[1]}`;
41
+ return {
42
+ series: DEFAULT_SERIES,
43
+ stageDir,
44
+ fullStageId: `${DEFAULT_STAGE_VERSION}-${stageDir}`,
45
+ };
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * stageId → plan 相对目录(不含 .openfeel 前缀)
51
+ * 例:v1.0.0-stage-34 → 'plan/v1/stage-34/';stage-01 → 'plan/v1/stage-01/'
52
+ * 无法解析返回 null。
53
+ */
54
+ export function stageIdToPlanDir(stageId) {
55
+ const parsed = parseStageId(stageId);
56
+ if (!parsed) {
57
+ return null;
58
+ }
59
+ return `plan/${parsed.series}/${parsed.stageDir}/`;
60
+ }
61
+ /**
62
+ * 短名/完整 stageId → 规范化完整 stageId
63
+ * 短名 stage-01 → v1.0.0-stage-01;完整/历史格式原样返回。
64
+ * 无法解析返回 null。
65
+ */
66
+ export function normalizeStageId(stageId) {
67
+ const parsed = parseStageId(stageId);
68
+ return parsed ? parsed.fullStageId : null;
69
+ }
70
+ /**
71
+ * 目录名(stage-NN)→ 完整 stageId(反向映射,回查 flow.json stages 键)
72
+ *
73
+ * 目录名 stage-33 无法唯一还原版本前缀,须回查 flow.json 匹配 `*-stage-33`。
74
+ * 去歧义规则(遗留风险 1):若多个 stageId 匹配 `*-stage-{NN}`,
75
+ * 1. 优先 pipeline.current.stage(若在匹配集中);
76
+ * 2. 否则按版本前缀字典序降序取最新(v1.0.0 > v0.9 > v0.5)。
77
+ * 注:同 stage-NN 跨多版本前缀重复在理论上不应发生,此规则仅兜底。
78
+ *
79
+ * @returns 完整 stageId,找不到匹配或 flow.json 不可读时返回 null
80
+ */
81
+ export function planDirToStageId(projectPath, stageDir) {
82
+ const flowPath = resolve(projectPath, '.openfeel', 'flow.json');
83
+ if (!existsSync(flowPath)) {
84
+ return null;
85
+ }
86
+ let flowData;
87
+ try {
88
+ flowData = JSON.parse(readFileSync(flowPath, 'utf-8'));
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ const stages = flowData.stages ?? {};
94
+ const suffix = `-${stageDir}`; // -stage-33
95
+ const matches = Object.keys(stages).filter((k) => k.endsWith(suffix));
96
+ if (matches.length === 0) {
97
+ return null;
98
+ }
99
+ if (matches.length === 1) {
100
+ return matches[0];
101
+ }
102
+ // 去歧义:优先当前阶段,否则取版本字典序最新
103
+ const current = flowData.pipeline?.current?.stage;
104
+ if (current && matches.includes(current)) {
105
+ return current;
106
+ }
107
+ return matches.slice().sort().at(-1) ?? matches[0];
108
+ }
109
+ /**
110
+ * 三级回退查找 status.md(决策 4,供 flow-manager 与命令层复用)
111
+ *
112
+ * 1. plan/{series}/stage-NN/status.md(解析 stageId → 精确路径,首选)
113
+ * 2. plan/ 下递归搜索 stage-NN/status.md(fast-glob 递归,兼容 series 变化/旧平铺)
114
+ * 3. stages/{stageId}/status.md(历史遗留,只读兜底)
115
+ *
116
+ * 性能说明(遗留风险 3):第 1 级精确路径命中率最高(新写入均在此),
117
+ * 第 2 级 fast-glob 仅在精确未命中时触发(兼容旧数据/系列变化的罕见场景),
118
+ * 因此对 flow status 高频调用性能影响可控;本 stage 不做缓存,留作后续观察。
119
+ *
120
+ * @returns status.md 绝对路径,三级均未命中返回 null
121
+ */
122
+ export function findStageStatusPath(projectPath, stageId) {
123
+ const parsed = parseStageId(stageId);
124
+ if (parsed) {
125
+ // 1. 精确路径 plan/{series}/stage-NN/status.md
126
+ const exact = resolve(projectPath, '.openfeel', 'plan', parsed.series, parsed.stageDir, 'status.md');
127
+ if (existsSync(exact)) {
128
+ return exact;
129
+ }
130
+ // 2. 递归搜索 plan/**/stage-NN/status.md
131
+ const planDir = resolve(projectPath, '.openfeel', 'plan');
132
+ if (existsSync(planDir)) {
133
+ const matches = fastGlob.sync(`**/${parsed.stageDir}/status.md`, {
134
+ cwd: planDir,
135
+ onlyFiles: true,
136
+ caseSensitiveMatch: true,
137
+ });
138
+ if (matches.length > 0) {
139
+ return resolve(planDir, matches[0]);
140
+ }
141
+ }
142
+ }
143
+ // 3. 历史兜底 stages/{stageId}/status.md
144
+ const legacy = resolve(projectPath, '.openfeel', 'stages', stageId, 'status.md');
145
+ if (existsSync(legacy)) {
146
+ return legacy;
147
+ }
148
+ return null;
149
+ }
150
+ //# sourceMappingURL=path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../../../src/core/plan/path.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"roadmap.d.ts","sourceRoot":"","sources":["../../../src/core/plan/roadmap.ts"],"names":[],"mappings":"AAOA,WAAW;AACX,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAOD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAoCxE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA0BzE"}
1
+ {"version":3,"file":"roadmap.d.ts","sourceRoot":"","sources":["../../../src/core/plan/roadmap.ts"],"names":[],"mappings":"AAQA,WAAW;AACX,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAOD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAoCxE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA0BzE"}
@@ -2,8 +2,9 @@
2
2
  * 分期大纲管理
3
3
  * 负责 .openfeel/roadmap/ 下的版本大纲文件创建与读取
4
4
  */
5
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
6
6
  import { resolve, join } from 'node:path';
7
+ import { atomicWriteFileSync } from '../fs/atomic-write.js';
7
8
  /** 规范化版本号:去掉用户可能输入的前导 v */
8
9
  function normalizeVersion(version) {
9
10
  return version.replace(/^v/, '');
@@ -41,7 +42,7 @@ export function createRoadmap(projectPath, version) {
41
42
 
42
43
  > 待补充
43
44
  `;
44
- writeFileSync(filePath, content, 'utf-8');
45
+ atomicWriteFileSync(filePath, content);
45
46
  console.log(`已创建分期大纲: v${ver}.md`);
46
47
  }
47
48
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"roadmap.js","sourceRoot":"","sources":["../../../src/core/plan/roadmap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1F,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAS1C,2BAA2B;AAC3B,SAAS,gBAAgB,CAAC,OAAe;IACvC,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,WAAmB,EAAE,OAAe;IAChE,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;IAEhE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,KAAK,CAAC,CAAC;IAEhD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,KAAK,CAAC,CAAC;QACjC,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,WAAW,GAAG;;;;;;;;;;;;;;;;;CAiB/B,CAAC;IAEA,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,KAAK,CAAC,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,WAAmB,EAAE,OAAgB;IAC/D,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;IAEhE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,kCAAkC,CAAC;IAC5C,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,KAAK,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,KAAK,CAAC,iBAAiB,GAAG,KAAK,CAAC,CAAC;YACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAC1C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAC9C,CAAC;IAEF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"roadmap.js","sourceRoot":"","sources":["../../../src/core/plan/roadmap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAS5D,2BAA2B;AAC3B,SAAS,gBAAgB,CAAC,OAAe;IACvC,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,WAAmB,EAAE,OAAe;IAChE,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;IAEhE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,KAAK,CAAC,CAAC;IAEhD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,KAAK,CAAC,CAAC;QACjC,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,WAAW,GAAG;;;;;;;;;;;;;;;;;CAiB/B,CAAC;IAEA,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACvC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,KAAK,CAAC,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,WAAmB,EAAE,OAAgB;IAC/D,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;IAEhE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,kCAAkC,CAAC;IAC5C,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,KAAK,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,KAAK,CAAC,iBAAiB,GAAG,KAAK,CAAC,CAAC;YACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAC1C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAC9C,CAAC;IAEF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -8,21 +8,22 @@ export interface Scheme {
8
8
  title: string;
9
9
  /** Markdown 全文 */
10
10
  content: string;
11
- /** 相对路径 .openfeel/stages/{stage}/ops/op-001_{title}.md */
11
+ /** 相对路径 .openfeel/plan/{series}/{stage}/ops/op-001_{title}.md */
12
12
  filePath: string;
13
13
  }
14
14
  /**
15
15
  * 创建操作方案
16
- * 在 .openfeel/stages/{stage}/ops/ 下创建 op-NNN_{title}.md
16
+ * 在 .openfeel/plan/{series}/{stage}/ops/ 下创建 op-NNN_{title}.md
17
17
  * NNN 自动递增(从该阶段的已有方案中计算)
18
18
  * 必须按固定模板生成,包含:目标、实施步骤(checkbox)、产出文件、自测清单、修正记录
19
19
  * 创建后自动同步到 flow.json(如果存在)
20
+ * @param stageName 阶段名(短名 stage-01 或完整 v1.0.0-stage-01 均可)
20
21
  * @returns opId(如 op-001)
21
22
  */
22
23
  export declare function createScheme(projectPath: string, stageName: string, title: string): string;
23
24
  /**
24
25
  * 读取操作方案
25
- * @param opId 操作ID(如 op-001)或完整 opId(如 stage-01.op-001)
26
+ * @param opId 操作ID(如 op-001)或完整 opId(如 stage-01.op-001 / v1.0.0-stage-01.op-001)
26
27
  * @returns Scheme 或 null
27
28
  */
28
29
  export declare function getScheme(projectPath: string, opId: string): Scheme | null;
@@ -1 +1 @@
1
- {"version":3,"file":"scheme.d.ts","sourceRoot":"","sources":["../../../src/core/plan/scheme.ts"],"names":[],"mappings":"AASA,WAAW;AACX,MAAM,WAAW,MAAM;IACrB,eAAe;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,WAAW;IACX,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoID;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAwB1F;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA4D1E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAyD7E"}
1
+ {"version":3,"file":"scheme.d.ts","sourceRoot":"","sources":["../../../src/core/plan/scheme.ts"],"names":[],"mappings":"AAaA,WAAW;AACX,MAAM,WAAW,MAAM;IACrB,eAAe;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,WAAW;IACX,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;CAClB;AA0HD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAkC1F;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA+B1E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CA6E7E"}
@@ -1,11 +1,15 @@
1
1
  /**
2
- * 操作方案管理(Schemer 产出层)
3
- * 负责 .openfeel/stages/{stage}/ops/ 下的操作方案文件 CRUD
2
+ * 操作方案管理(openfeel-schemer 产出层)
3
+ * 负责 .openfeel/plan/{series}/{stage}/ops/ 下的操作方案文件 CRUD
4
4
  * 创建后自动同步到 flow.json 的 stages/{stage}.ops 中
5
5
  */
6
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
7
7
  import { resolve, join } from 'node:path';
8
- import { FlowManager } from '../flow-manager.js';
8
+ import { FlowManager, isFlowConcurrentError } from '../flow-manager.js';
9
+ import { parseStageId } from './path.js';
10
+ import { atomicWriteFileSync } from '../fs/atomic-write.js';
11
+ import { withFileLock, projectLockPath } from '../fs/file-lock.js';
12
+ import { reserveSequence } from '../fs/sequence.js';
9
13
  /**
10
14
  * 生成操作方案的固定 Markdown 模板
11
15
  */
@@ -15,7 +19,7 @@ function generateSchemeTemplate(opId, stageName, title) {
15
19
  - **阶段**:${stageName}
16
20
  - **状态**:pending
17
21
  - **前置**:无
18
- - **负责 Agent**:Executor
22
+ - **负责 Agent**:openfeel-executor
19
23
  - **最多重试**:3
20
24
 
21
25
  ## 目标
@@ -52,29 +56,14 @@ function extractTitle(fileName) {
52
56
  const noPrefix = fileName.replace(/^op-\d+_?/, '');
53
57
  return noPrefix.replace(/\.md$/, '').replace(/_/g, ' ');
54
58
  }
55
- /**
56
- * 计算下一个 opId(基于已有方案中的最大编号 +1)
57
- */
58
- function getNextOpId(opsDir) {
59
- if (!existsSync(opsDir)) {
60
- return 'op-001';
61
- }
62
- const files = readdirSync(opsDir).filter((f) => f.endsWith('.md'));
63
- let maxNum = 0;
64
- for (const file of files) {
65
- const match = file.match(/^op-(\d+)/);
66
- if (match) {
67
- const num = parseInt(match[1], 10);
68
- if (num > maxNum) {
69
- maxNum = num;
70
- }
71
- }
72
- }
73
- return `op-${String(maxNum + 1).padStart(3, '0')}`;
59
+ /** 序号 → opId(3 位补零) */
60
+ function opIdOf(seq) {
61
+ return `op-${String(seq).padStart(3, '0')}`;
74
62
  }
75
63
  /**
76
64
  * 同步方案到 flow.json
77
65
  * 若 flow.json 不存在或对应 stage 不存在,跳过同步(不报错)
66
+ * @param stageName 完整 stageId(如 v1.0.0-stage-01,调用方已规范化)
78
67
  */
79
68
  function syncToFlowJson(projectPath, stageName, opId, title) {
80
69
  const flowJsonPath = resolve(projectPath, '.openfeel', 'flow.json');
@@ -103,7 +92,7 @@ function syncToFlowJson(projectPath, stageName, opId, title) {
103
92
  id: opId,
104
93
  title,
105
94
  state: 'pending',
106
- assignee: 'Executor',
95
+ assignee: 'openfeel-executor',
107
96
  attempts: 0,
108
97
  max_attempts: 3,
109
98
  checkpoints: {
@@ -116,91 +105,88 @@ function syncToFlowJson(projectPath, stageName, opId, title) {
116
105
  };
117
106
  flowMgr.save();
118
107
  }
119
- catch {
120
- // 同步失败不阻塞方案创建(静默忽略)
108
+ catch (err) {
109
+ // 并发冲突:op 文件已创建,但 flow.json 注册失败;不静默吞错,告警并提示兜底
110
+ if (isFlowConcurrentError(err)) {
111
+ console.warn(`[WARN] op ${opId} 已创建,但 flow.json 同步因并发冲突失败;` +
112
+ `请执行 openfeel flow repair 兜底或重新注册该 op。`);
113
+ return;
114
+ }
115
+ // 其它同步失败不阻塞方案创建(既有语义:静默忽略)
121
116
  }
122
117
  }
123
118
  /**
124
119
  * 创建操作方案
125
- * 在 .openfeel/stages/{stage}/ops/ 下创建 op-NNN_{title}.md
120
+ * 在 .openfeel/plan/{series}/{stage}/ops/ 下创建 op-NNN_{title}.md
126
121
  * NNN 自动递增(从该阶段的已有方案中计算)
127
122
  * 必须按固定模板生成,包含:目标、实施步骤(checkbox)、产出文件、自测清单、修正记录
128
123
  * 创建后自动同步到 flow.json(如果存在)
124
+ * @param stageName 阶段名(短名 stage-01 或完整 v1.0.0-stage-01 均可)
129
125
  * @returns opId(如 op-001)
130
126
  */
131
127
  export function createScheme(projectPath, stageName, title) {
132
- // 1. 确保 .openfeel/stages/{stage}/ops/ 目录存在
133
- const opsDir = resolve(projectPath, '.openfeel', 'stages', stageName, 'ops');
128
+ // 解析 stageId(短名/完整)得到 series + stageDir + 完整 ID
129
+ const parsed = parseStageId(stageName);
130
+ if (!parsed) {
131
+ throw new Error(`非法阶段名: ${stageName}(应为 stage-NN 或 vX.Y.Z.W-stage-NN)`);
132
+ }
133
+ // 1. 确保 .openfeel/plan/{series}/{stageDir}/ops/ 目录存在
134
+ const opsDir = resolve(projectPath, '.openfeel', 'plan', parsed.series, parsed.stageDir, 'ops');
134
135
  if (!existsSync(opsDir)) {
135
136
  mkdirSync(opsDir, { recursive: true });
136
137
  }
137
- // 2. 计算下一个 opId
138
- const opId = getNextOpId(opsDir);
139
- // 3. 生成模板内容并写入文件
140
- // 文件名中空格用下划线替换
138
+ // 2. 锁内:原子占号 + 原子写 op 文件(临界区仅含占号与写文件,不含 flow 同步)
141
139
  const safeTitle = title.replace(/\s+/g, '_');
142
- const fileName = `${opId}_${safeTitle}.md`;
143
- const filePath = join(opsDir, fileName);
144
- const content = generateSchemeTemplate(opId, stageName, title);
145
- writeFileSync(filePath, content, 'utf-8');
146
- // 4. 同步到 flow.json
147
- syncToFlowJson(projectPath, stageName, opId, title);
148
- // 5. 返回 opId
140
+ const lockPath = projectLockPath(projectPath, `scheme-${parsed.stageDir}`);
141
+ const opId = withFileLock(lockPath, () => {
142
+ const reserved = reserveSequence({
143
+ dir: opsDir,
144
+ candidate: (seq) => `${opIdOf(seq)}_${safeTitle}.md`,
145
+ parse: (fileName) => {
146
+ const m = fileName.match(/^op-(\d+)/);
147
+ return m ? parseInt(m[1], 10) : null;
148
+ },
149
+ });
150
+ const content = generateSchemeTemplate(opIdOf(reserved.seq), parsed.fullStageId, title);
151
+ atomicWriteFileSync(reserved.path, content);
152
+ return opIdOf(reserved.seq);
153
+ });
154
+ // 3. 同步到 flow.json(键用完整 stageId;由 FlowManager.save 的 flow.lock 保护)
155
+ syncToFlowJson(projectPath, parsed.fullStageId, opId, title);
149
156
  return opId;
150
157
  }
151
158
  /**
152
159
  * 读取操作方案
153
- * @param opId 操作ID(如 op-001)或完整 opId(如 stage-01.op-001)
160
+ * @param opId 操作ID(如 op-001)或完整 opId(如 stage-01.op-001 / v1.0.0-stage-01.op-001)
154
161
  * @returns Scheme 或 null
155
162
  */
156
163
  export function getScheme(projectPath, opId) {
157
- const stagesDir = resolve(projectPath, '.openfeel', 'stages');
158
- if (!existsSync(stagesDir)) {
159
- return null;
160
- }
161
- let targetStageName;
164
+ let targetStage = null;
162
165
  let targetOpId;
163
- // 判断 opId 格式:包含 "." 则为完整格式 "stage-01.op-001"
164
- const dotIdx = opId.indexOf('.');
165
- if (dotIdx !== -1) {
166
- targetStageName = opId.substring(0, dotIdx);
167
- targetOpId = opId.substring(dotIdx + 1);
166
+ // 解析 opId:'{stage}.{op-XXX}' 或纯 '{op-XXX}'
167
+ // 用正则锚定尾部 .op-NNN,避免完整 stageId 版本号中的点号干扰
168
+ const match = opId.match(/^(.+)\.(op-\d+)$/);
169
+ if (match) {
170
+ targetStage = match[1];
171
+ targetOpId = match[2];
168
172
  }
169
173
  else {
170
- targetStageName = ''; // 遍历所有阶段查找
174
+ targetStage = null;
171
175
  targetOpId = opId;
172
176
  }
173
- // 读取 stages 目录下的子目录
174
- const stageEntries = readdirSync(stagesDir, { withFileTypes: true });
175
- for (const stageEntry of stageEntries) {
176
- if (!stageEntry.isDirectory()) {
177
- continue;
178
- }
179
- // 若指定了阶段名,只检查匹配的目录
180
- if (targetStageName && stageEntry.name !== targetStageName) {
181
- continue;
177
+ // 若指定阶段,解析出 stageDir 后遍历查找
178
+ let targetStageDir = null;
179
+ if (targetStage) {
180
+ const parsed = parseStageId(targetStage);
181
+ if (!parsed) {
182
+ return null; // 阶段名非法
182
183
  }
183
- const opsDir = join(stagesDir, stageEntry.name, 'ops');
184
- if (!existsSync(opsDir)) {
185
- continue;
186
- }
187
- // 在 ops 目录中查找匹配的文件
188
- const opFiles = readdirSync(opsDir).filter((f) => f.startsWith(targetOpId) && f.endsWith('.md'));
189
- if (opFiles.length === 0) {
190
- continue;
184
+ targetStageDir = parsed.stageDir;
185
+ }
186
+ for (const scheme of listSchemes(projectPath, targetStageDir ?? undefined)) {
187
+ if (scheme.opId === targetOpId) {
188
+ return scheme;
191
189
  }
192
- // 找到匹配文件,读取内容
193
- const matchedFile = opFiles[0]; // 取第一个匹配
194
- const filePath = join(opsDir, matchedFile);
195
- const content = readFileSync(filePath, 'utf-8');
196
- const scheme = {
197
- opId: targetOpId,
198
- stage: stageEntry.name,
199
- title: extractTitle(matchedFile),
200
- content,
201
- filePath: `.openfeel/stages/${stageEntry.name}/ops/${matchedFile}`,
202
- };
203
- return scheme;
204
190
  }
205
191
  return null;
206
192
  }
@@ -209,44 +195,61 @@ export function getScheme(projectPath, opId) {
209
195
  * @param stageName 可选,不传则列出所有阶段的方案
210
196
  */
211
197
  export function listSchemes(projectPath, stageName) {
212
- const stagesDir = resolve(projectPath, '.openfeel', 'stages');
198
+ const planDir = resolve(projectPath, '.openfeel', 'plan');
213
199
  const result = [];
214
- if (!existsSync(stagesDir)) {
200
+ if (!existsSync(planDir)) {
215
201
  return result;
216
202
  }
217
- // 若指定了阶段名,只遍历该阶段
218
- const stageNames = [];
203
+ // 解析目标 stageDir(若指定阶段过滤)
204
+ let targetStageDir = null;
219
205
  if (stageName) {
220
- stageNames.push(stageName);
221
- }
222
- else {
223
- const stageEntries = readdirSync(stagesDir, { withFileTypes: true });
224
- for (const entry of stageEntries) {
225
- if (entry.isDirectory()) {
226
- stageNames.push(entry.name);
227
- }
206
+ const parsed = parseStageId(stageName);
207
+ if (!parsed) {
208
+ return result; // 阶段名非法时返回空
228
209
  }
210
+ targetStageDir = parsed.stageDir;
229
211
  }
230
- for (const sn of stageNames) {
231
- const opsDir = join(stagesDir, sn, 'ops');
232
- if (!existsSync(opsDir)) {
212
+ // 遍历 plan/{series}/stage-NN/ 两层目录
213
+ const seriesEntries = readdirSync(planDir, { withFileTypes: true });
214
+ for (const seriesEntry of seriesEntries) {
215
+ if (!seriesEntry.isDirectory()) {
233
216
  continue;
234
217
  }
235
- const opFiles = readdirSync(opsDir).filter((f) => f.endsWith('.md'));
236
- for (const fileName of opFiles) {
237
- const opId = extractOpId(fileName);
238
- if (!opId) {
218
+ const seriesDir = join(planDir, seriesEntry.name);
219
+ let stageEntries;
220
+ try {
221
+ stageEntries = readdirSync(seriesDir, { withFileTypes: true });
222
+ }
223
+ catch {
224
+ continue;
225
+ }
226
+ for (const stageEntry of stageEntries) {
227
+ if (!stageEntry.isDirectory()) {
228
+ continue;
229
+ }
230
+ if (targetStageDir && stageEntry.name !== targetStageDir) {
231
+ continue;
232
+ }
233
+ const opsDir = join(seriesDir, stageEntry.name, 'ops');
234
+ if (!existsSync(opsDir)) {
239
235
  continue;
240
236
  }
241
- const filePath = join(opsDir, fileName);
242
- const content = readFileSync(filePath, 'utf-8');
243
- result.push({
244
- opId,
245
- stage: sn,
246
- title: extractTitle(fileName),
247
- content,
248
- filePath: `.openfeel/stages/${sn}/ops/${fileName}`,
249
- });
237
+ const opFiles = readdirSync(opsDir).filter((f) => f.endsWith('.md'));
238
+ for (const fileName of opFiles) {
239
+ const opId = extractOpId(fileName);
240
+ if (!opId) {
241
+ continue;
242
+ }
243
+ const filePath = join(opsDir, fileName);
244
+ const content = readFileSync(filePath, 'utf-8');
245
+ result.push({
246
+ opId,
247
+ stage: stageEntry.name,
248
+ title: extractTitle(fileName),
249
+ content,
250
+ filePath: `.openfeel/plan/${seriesEntry.name}/${stageEntry.name}/ops/${fileName}`,
251
+ });
252
+ }
250
253
  }
251
254
  }
252
255
  // 按阶段名 + opId 排序