openfeel 1.1.7 → 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 (115) hide show
  1. package/README.en.md +35 -8
  2. package/README.md +29 -4
  3. package/README.zh-CN.md +35 -8
  4. package/bin/openfeel.js +3 -2
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +7 -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 +29 -4
  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 +385 -14
  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 +19 -6
  16. package/dist/commands/plan.js.map +1 -1
  17. package/dist/commands/req.d.ts +15 -0
  18. package/dist/commands/req.d.ts.map +1 -0
  19. package/dist/commands/req.js +735 -0
  20. package/dist/commands/req.js.map +1 -0
  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-archive/archive-file.d.ts +35 -0
  29. package/dist/core/flow-archive/archive-file.d.ts.map +1 -0
  30. package/dist/core/flow-archive/archive-file.js +3 -0
  31. package/dist/core/flow-archive/archive-file.js.map +1 -0
  32. package/dist/core/flow-archive/archive-health.d.ts +34 -0
  33. package/dist/core/flow-archive/archive-health.d.ts.map +1 -0
  34. package/dist/core/flow-archive/archive-health.js +76 -0
  35. package/dist/core/flow-archive/archive-health.js.map +1 -0
  36. package/dist/core/flow-archive/archive-ops.d.ts +68 -0
  37. package/dist/core/flow-archive/archive-ops.d.ts.map +1 -0
  38. package/dist/core/flow-archive/archive-ops.js +184 -0
  39. package/dist/core/flow-archive/archive-ops.js.map +1 -0
  40. package/dist/core/flow-archive/grouping.d.ts +58 -0
  41. package/dist/core/flow-archive/grouping.d.ts.map +1 -0
  42. package/dist/core/flow-archive/grouping.js +153 -0
  43. package/dist/core/flow-archive/grouping.js.map +1 -0
  44. package/dist/core/flow-manager.d.ts +67 -9
  45. package/dist/core/flow-manager.d.ts.map +1 -1
  46. package/dist/core/flow-manager.js +243 -51
  47. package/dist/core/flow-manager.js.map +1 -1
  48. package/dist/core/i18n-data/en.d.ts +1 -0
  49. package/dist/core/i18n-data/en.d.ts.map +1 -1
  50. package/dist/core/i18n-data/en.js +174 -4
  51. package/dist/core/i18n-data/en.js.map +1 -1
  52. package/dist/core/i18n-data/zh-CN.d.ts +1 -0
  53. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  54. package/dist/core/i18n-data/zh-CN.js +175 -4
  55. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  56. package/dist/core/i18n.js +4 -4
  57. package/dist/core/i18n.js.map +1 -1
  58. package/dist/core/managed-region.d.ts +16 -2
  59. package/dist/core/managed-region.d.ts.map +1 -1
  60. package/dist/core/managed-region.js +24 -3
  61. package/dist/core/managed-region.js.map +1 -1
  62. package/dist/core/metrics.d.ts +2 -0
  63. package/dist/core/metrics.d.ts.map +1 -1
  64. package/dist/core/metrics.js +15 -0
  65. package/dist/core/metrics.js.map +1 -1
  66. package/dist/core/model-config.d.ts +1 -1
  67. package/dist/core/model-config.d.ts.map +1 -1
  68. package/dist/core/model-config.js +2 -2
  69. package/dist/core/model-config.js.map +1 -1
  70. package/dist/core/opencode-config.js +1 -1
  71. package/dist/core/opencode-config.js.map +1 -1
  72. package/dist/core/pipeline-schema.d.ts +15 -0
  73. package/dist/core/pipeline-schema.d.ts.map +1 -1
  74. package/dist/core/pipeline-schema.js +17 -0
  75. package/dist/core/pipeline-schema.js.map +1 -1
  76. package/dist/core/plan/path.d.ts +17 -0
  77. package/dist/core/plan/path.d.ts.map +1 -1
  78. package/dist/core/plan/path.js +31 -0
  79. package/dist/core/plan/path.js.map +1 -1
  80. package/dist/core/plan/stage.d.ts +2 -1
  81. package/dist/core/plan/stage.d.ts.map +1 -1
  82. package/dist/core/plan/stage.js +7 -2
  83. package/dist/core/plan/stage.js.map +1 -1
  84. package/dist/core/req/aggregate.d.ts +62 -0
  85. package/dist/core/req/aggregate.d.ts.map +1 -0
  86. package/dist/core/req/aggregate.js +134 -0
  87. package/dist/core/req/aggregate.js.map +1 -0
  88. package/dist/core/req/body-file.d.ts +16 -0
  89. package/dist/core/req/body-file.d.ts.map +1 -0
  90. package/dist/core/req/body-file.js +84 -0
  91. package/dist/core/req/body-file.js.map +1 -0
  92. package/dist/core/req/requirement-ops.d.ts +116 -0
  93. package/dist/core/req/requirement-ops.d.ts.map +1 -0
  94. package/dist/core/req/requirement-ops.js +395 -0
  95. package/dist/core/req/requirement-ops.js.map +1 -0
  96. package/dist/core/req/types.d.ts +46 -0
  97. package/dist/core/req/types.d.ts.map +1 -0
  98. package/dist/core/req/types.js +7 -0
  99. package/dist/core/req/types.js.map +1 -0
  100. package/dist/core/runtime-log.d.ts +18 -0
  101. package/dist/core/runtime-log.d.ts.map +1 -1
  102. package/dist/core/runtime-log.js +72 -0
  103. package/dist/core/runtime-log.js.map +1 -1
  104. package/dist/core/session.d.ts +24 -0
  105. package/dist/core/session.d.ts.map +1 -0
  106. package/dist/core/session.js +107 -0
  107. package/dist/core/session.js.map +1 -0
  108. package/dist/core/setup.js +4 -4
  109. package/dist/core/template-loader.d.ts.map +1 -1
  110. package/dist/core/template-loader.js +3160 -6571
  111. package/dist/core/template-loader.js.map +1 -1
  112. package/dist/core/update.d.ts.map +1 -1
  113. package/dist/core/update.js +770 -26
  114. package/dist/core/update.js.map +1 -1
  115. package/package.json +1 -1
@@ -0,0 +1,735 @@
1
+ import { t, getCliLang } from '../core/i18n.js';
2
+ import { FlowManager, isFlowConcurrentError } from '../core/flow-manager.js';
3
+ import { handleConcurrentConflict } from './shared/errors.js';
4
+ import { addRequirement, getRequirement, getRequirements, listRequirements, claimRequirement, releaseRequirement, updateRequirement, advanceRequirement, linkRequirement, getAllowedNextStatuses, splitRequirement, } from '../core/req/requirement-ops.js';
5
+ import { REQ_STATUSES, REQ_ID_PATTERN } from '../core/req/types.js';
6
+ import { writeRequirementBodySkeleton } from '../core/req/body-file.js';
7
+ import { getUserName } from '../core/workspace/identity.js';
8
+ import { normalizeStageId } from '../core/plan/path.js';
9
+ /** 创建 FlowManager(当前工作目录) */
10
+ function createManager() {
11
+ return new FlowManager(process.cwd());
12
+ }
13
+ /** JSON 失败载荷:纯单文档走 stdout(不混 stderr——对齐既有 flow archive 范式),并置退出码 1 */
14
+ function failJson(payload) {
15
+ console.log(JSON.stringify({ schemaVersion: 1, ...payload }, null, 2));
16
+ process.exitCode = 1;
17
+ }
18
+ /**
19
+ * 判定是否走 `--json`:子命令显式 `--json` 或父级命令 `req` 的 `--json`(防御合并)。
20
+ * 注:父级 `req` 当前未声明 `--json`,取值为 `undefined` 不影响本判定。
21
+ */
22
+ function asJsonOf(options, command) {
23
+ return options.json === true || command.parent?.opts().json === true;
24
+ }
25
+ /** 未初始化分流:JSON 走 `no-init` 纯文档;human 走 `common.errorNoInit`;均 exit 1 */
26
+ function failNoInit(lang, asJson) {
27
+ if (asJson) {
28
+ failJson({ error: 'no-init' });
29
+ return;
30
+ }
31
+ console.error(t('common.errorNoInit', lang));
32
+ process.exitCode = 1;
33
+ }
34
+ /** 需求 ID 格式校验(取 core 常量单点,防 typo) */
35
+ function isValidReqId(id) {
36
+ return REQ_ID_PATTERN.test(id);
37
+ }
38
+ /** --into 累积(repeatable option;**本仓首个 repeatable**——既有先例为 variadic `<ids...>`(`flow.ts:665` 选项描述自警「建议置于命令末尾」),其有吞后续参数风险故不采用) */
39
+ function collectInto(value, previous) {
40
+ return [...previous, value];
41
+ }
42
+ /** 引擎错误码 → i18n 文案映射(命令层仅映射,零校验);未登记码走通用模板兜底 */
43
+ function failCode(lang, code, msgs) {
44
+ console.error(msgs[code] ?? t('common.errorTmpl', lang, { msg: code }));
45
+ process.exitCode = 1;
46
+ }
47
+ /** 通用错误文案 + 退出码 1(非并发异常统一出口;不 return,由调用方 return) */
48
+ function failGeneric(lang, err) {
49
+ const msg = err instanceof Error ? err.message : String(err);
50
+ console.error(t('common.errorTmpl', lang, { msg }));
51
+ process.exitCode = 1;
52
+ }
53
+ /**
54
+ * 保存并收敛并发:成功返回 true;并发冲突 → 单点 exit 2(不返回);其他异常 → 通用文案 exit 1 并返回 false。
55
+ */
56
+ function saveOrFail(mgr, lang) {
57
+ try {
58
+ mgr.save();
59
+ return true;
60
+ }
61
+ catch (err) {
62
+ if (isFlowConcurrentError(err)) {
63
+ handleConcurrentConflict(err, lang);
64
+ }
65
+ else {
66
+ failGeneric(lang, err);
67
+ }
68
+ return false;
69
+ }
70
+ }
71
+ /** `req` 命令组注册入口 */
72
+ export function registerReqCommand(program) {
73
+ const req = program
74
+ .command('req')
75
+ .description('需求管理(需求流转命令组)');
76
+ // ── req add <title> [--target-version <v>] [--priority <p>] ──
77
+ req
78
+ .command('add <title>')
79
+ .description('创建需求(ID 自动分配;正文骨架铺到 .openfeel/requirements/)')
80
+ .option('--target-version <version>', '预估版本(如 v1.3.0)')
81
+ .option('--priority <priority>', '优先级(P0/P1/P2)')
82
+ .option('--workflow-mode <mode>', '工作流模式(full / fast / general;缺省不设置)')
83
+ .action((title, options) => {
84
+ const lang = getCliLang(process.cwd());
85
+ const mgr = createManager();
86
+ if (!mgr.isLoaded()) {
87
+ failNoInit(lang, false);
88
+ return;
89
+ }
90
+ const data = mgr.getData();
91
+ try {
92
+ // 运行期值域由引擎单点判定(命令层零校验);priority / workflow_mode 断言仅编译期
93
+ const created = addRequirement(data, {
94
+ title,
95
+ target_version: options.targetVersion,
96
+ priority: options.priority,
97
+ workflow_mode: options.workflowMode,
98
+ });
99
+ // 正文骨架:严格直传引擎返回本体(禁浅副本);写失败 → 不 save(flow.json 零变更)
100
+ let bodyPath;
101
+ try {
102
+ bodyPath = writeRequirementBodySkeleton(process.cwd(), created);
103
+ }
104
+ catch (err) {
105
+ failGeneric(lang, err);
106
+ return;
107
+ }
108
+ if (!saveOrFail(mgr, lang)) {
109
+ return;
110
+ }
111
+ console.log(t('req.add.doneTmpl', lang, { id: created.id, title: created.title }));
112
+ console.log(t('req.add.bodyTmpl', lang, { path: bodyPath }));
113
+ }
114
+ catch (err) {
115
+ // 唯一抛错路径(空 title / 非法 priority / 空 target_version)→ 通用模板 exit 1
116
+ failGeneric(lang, err);
117
+ }
118
+ });
119
+ // ── req list [--status <s>] [--target-version <v>] [--owner <o>] [--json] ──
120
+ req
121
+ .command('list')
122
+ .description('列出需求(可按状态/版本/认领人过滤)')
123
+ .option('--status <status>', '按状态过滤(draft/scheduled/in_progress/done/blocked/cancelled/rejected)')
124
+ .option('--target-version <version>', '按预估版本过滤')
125
+ .option('--owner <owner>', '按认领人过滤')
126
+ .option('--json', '以 JSON 输出 { schemaVersion, requirements }')
127
+ .action((options, command) => {
128
+ const lang = getCliLang(process.cwd());
129
+ const asJson = asJsonOf(options, command);
130
+ const mgr = createManager();
131
+ if (!mgr.isLoaded()) {
132
+ failNoInit(lang, asJson);
133
+ return;
134
+ }
135
+ const data = mgr.getData();
136
+ // 读命令过滤值域仅取 core 常量防 typo;undefined = 未过滤(不得进非法分支)
137
+ if (options.status !== undefined && !REQ_STATUSES.includes(options.status)) {
138
+ if (asJson) {
139
+ failJson({ error: 'invalid-argument', field: 'status', value: options.status, allowed: [...REQ_STATUSES] });
140
+ }
141
+ else {
142
+ console.error(t('req.list.invalidStatusTmpl', lang, {
143
+ value: options.status,
144
+ allowed: REQ_STATUSES.join(', '),
145
+ }));
146
+ process.exitCode = 1;
147
+ }
148
+ return;
149
+ }
150
+ // 只读保证:不 save / 不 appendLog / 零写盘
151
+ const rows = listRequirements(data, {
152
+ status: options.status,
153
+ target_version: options.targetVersion,
154
+ owner: options.owner,
155
+ });
156
+ if (asJson) {
157
+ console.log(JSON.stringify({ schemaVersion: 1, requirements: rows }, null, 2));
158
+ return;
159
+ }
160
+ if (rows.length === 0) {
161
+ console.log(t('req.list.empty', lang));
162
+ return;
163
+ }
164
+ for (const r of rows) {
165
+ console.log(t('req.list.itemTmpl', lang, { id: r.id, status: r.status, title: r.title }));
166
+ }
167
+ });
168
+ // ── req show <id> [--json] ──
169
+ req
170
+ .command('show <id>')
171
+ .description('查看需求详情(关联阶段为原始只读枚举)')
172
+ .option('--json', '以 JSON 输出 { schemaVersion, requirement, stages, parent, children }')
173
+ .action((id, options, command) => {
174
+ const lang = getCliLang(process.cwd());
175
+ const asJson = asJsonOf(options, command);
176
+ const mgr = createManager();
177
+ if (!mgr.isLoaded()) {
178
+ failNoInit(lang, asJson);
179
+ return;
180
+ }
181
+ const data = mgr.getData();
182
+ if (!isValidReqId(id)) {
183
+ if (asJson) {
184
+ failJson({ error: 'invalid-argument', field: 'id', value: id });
185
+ }
186
+ else {
187
+ console.error(t('req.invalidIdTmpl', lang, { input: id }));
188
+ process.exitCode = 1;
189
+ }
190
+ return;
191
+ }
192
+ const r = getRequirement(data, id);
193
+ if (!r) {
194
+ if (asJson) {
195
+ failJson({ error: 'not_found', id });
196
+ }
197
+ else {
198
+ console.error(t('req.err.notFoundTmpl', lang, { id }));
199
+ process.exitCode = 1;
200
+ }
201
+ return;
202
+ }
203
+ // 关联阶段 = 原始只读枚举(零聚合计算 / 零阶段推进字段读取——归后续阶段)
204
+ const stages = Object.entries(data.stages ?? {})
205
+ .filter(([, s]) => (s.requirement_id ?? null) === id)
206
+ .map(([k]) => k);
207
+ // 子需求 = 存储顺序枚举(不额外排序——避免第二排序源;对齐 stages 无排序先例)
208
+ const childRows = getRequirements(data).filter((r) => r.parent_id === id);
209
+ const children = childRows.map((c) => c.id);
210
+ // 父需求 ID **原样回显**(悬空不报错、不做存在性判定——悬空对账职责单点在 requirementHealthItems,REV-87-004①)
211
+ const parent = r.parent_id ?? null;
212
+ if (asJson) {
213
+ console.log(JSON.stringify({ schemaVersion: 1, requirement: r, stages, parent, children }, null, 2));
214
+ return;
215
+ }
216
+ console.log(t('req.show.titleTmpl', lang, { id: r.id, title: r.title }));
217
+ console.log(t('req.show.metaTmpl', lang, {
218
+ status: r.status,
219
+ targetVersion: r.target_version ?? t('common.none', lang),
220
+ priority: r.priority ?? t('common.none', lang),
221
+ }));
222
+ console.log(r.owner
223
+ ? t('req.show.ownerTmpl', lang, { owner: r.owner, claimedAt: r.claimed_at ?? '' })
224
+ : t('req.show.ownerNone', lang));
225
+ console.log(t('req.show.timesTmpl', lang, { created: r.created_at, updated: r.updated_at }));
226
+ console.log(t('req.show.bodyTmpl', lang, { path: r.body_path ?? t('common.none', lang) }));
227
+ console.log(parent !== null
228
+ ? t('req.show.parentTmpl', lang, { parent })
229
+ : t('req.show.parentNone', lang));
230
+ if (childRows.length === 0) {
231
+ console.log(t('req.show.childrenNone', lang));
232
+ }
233
+ else {
234
+ console.log(t('req.show.childrenLabel', lang));
235
+ for (const c of childRows) {
236
+ console.log(t('req.show.childItemTmpl', lang, { id: c.id, status: c.status, title: c.title }));
237
+ }
238
+ }
239
+ if (stages.length === 0) {
240
+ console.log(t('req.show.stagesNone', lang));
241
+ }
242
+ else {
243
+ console.log(t('req.show.stagesLabel', lang));
244
+ for (const s of stages) {
245
+ console.log(t('req.show.stageItemTmpl', lang, { stage: s }));
246
+ }
247
+ }
248
+ });
249
+ // ── req claim <id> [--owner <name>] ──
250
+ req
251
+ .command('claim <id>')
252
+ .description('认领需求(占用校验;已被他人认领则拒绝)')
253
+ .option('--owner <owner>', '认领人(缺省:.openfeel/.info.json user → git config user.name)')
254
+ .action((id, options) => {
255
+ const lang = getCliLang(process.cwd());
256
+ const mgr = createManager();
257
+ if (!mgr.isLoaded()) {
258
+ failNoInit(lang, false);
259
+ return;
260
+ }
261
+ const data = mgr.getData();
262
+ if (!isValidReqId(id)) {
263
+ console.error(t('req.invalidIdTmpl', lang, { input: id }));
264
+ process.exitCode = 1;
265
+ return;
266
+ }
267
+ // --owner 缺省链:.info.json user → git config user.name → 哨兵 'unknown' 拒绝(禁硬编码)
268
+ let owner = options.owner;
269
+ if (owner === undefined) {
270
+ const resolved = getUserName(process.cwd());
271
+ if (resolved === '' || resolved === 'unknown') {
272
+ console.error(t('req.claim.ownerUnresolvedTmpl', lang));
273
+ process.exitCode = 1;
274
+ return;
275
+ }
276
+ owner = resolved;
277
+ }
278
+ // 返回码分流(命令层仅映射,零校验):占用 / 终态 / owner 空串均由引擎单点判定
279
+ const r = claimRequirement(data, id, owner);
280
+ if (!r.ok) {
281
+ failCode(lang, r.code, {
282
+ not_found: t('req.err.notFoundTmpl', lang, { id }),
283
+ already_claimed: t('req.err.alreadyClaimedTmpl', lang, { id }),
284
+ invalid_field: t('req.err.invalidFieldTmpl', lang, { id }),
285
+ invalid_status: t('req.claim.terminalTmpl', lang, { id, status: getRequirement(data, id)?.status ?? '' }),
286
+ });
287
+ return;
288
+ }
289
+ if (!r.changed) {
290
+ // 同 owner 幂等:不 save(revision 不变)
291
+ console.log(t('req.claim.idempotentTmpl', lang, { id, owner }));
292
+ return;
293
+ }
294
+ if (!saveOrFail(mgr, lang)) {
295
+ return;
296
+ }
297
+ console.log(t('req.claim.doneTmpl', lang, { id, owner }));
298
+ });
299
+ // ── req release <id> ──
300
+ req
301
+ .command('release <id>')
302
+ .description('释放认领(未认领为 no-op 成功)')
303
+ .action((id) => {
304
+ const lang = getCliLang(process.cwd());
305
+ const mgr = createManager();
306
+ if (!mgr.isLoaded()) {
307
+ failNoInit(lang, false);
308
+ return;
309
+ }
310
+ const data = mgr.getData();
311
+ if (!isValidReqId(id)) {
312
+ console.error(t('req.invalidIdTmpl', lang, { input: id }));
313
+ process.exitCode = 1;
314
+ return;
315
+ }
316
+ const r = releaseRequirement(data, id);
317
+ if (!r.ok) {
318
+ failCode(lang, r.code, { not_found: t('req.err.notFoundTmpl', lang, { id }) });
319
+ return;
320
+ }
321
+ if (!r.changed) {
322
+ // 未占用 no-op:不 save,恒 exit 0
323
+ console.log(t('req.release.noopTmpl', lang, { id }));
324
+ return;
325
+ }
326
+ if (!saveOrFail(mgr, lang)) {
327
+ return;
328
+ }
329
+ console.log(t('req.release.doneTmpl', lang, { id }));
330
+ });
331
+ // ── req set <id> [--status] [--title] [--target-version] [--priority] ──
332
+ req
333
+ .command('set <id>')
334
+ .description('更新需求字段(白名单;状态流转合法性由引擎判定)')
335
+ .option('--status <status>', '目标状态(合法性与 done 门由引擎判定)')
336
+ .option('--title <title>', '新标题')
337
+ .option('--target-version <version>', '新预估版本')
338
+ .option('--priority <priority>', '新优先级(P0/P1/P2)')
339
+ .option('--workflow-mode <mode>', '新工作流模式(full / fast / general;general 仅限无关联阶段的需求)')
340
+ .action((id, options) => {
341
+ const lang = getCliLang(process.cwd());
342
+ const mgr = createManager();
343
+ if (!mgr.isLoaded()) {
344
+ failNoInit(lang, false);
345
+ return;
346
+ }
347
+ const data = mgr.getData();
348
+ if (!isValidReqId(id)) {
349
+ console.error(t('req.invalidIdTmpl', lang, { input: id }));
350
+ process.exitCode = 1;
351
+ return;
352
+ }
353
+ // 无字段守卫(argument-shape,非业务校验)
354
+ if (options.status === undefined && options.title === undefined
355
+ && options.targetVersion === undefined && options.priority === undefined
356
+ && options.workflowMode === undefined) {
357
+ console.error(t('req.set.noFieldsTmpl', lang));
358
+ process.exitCode = 1;
359
+ return;
360
+ }
361
+ // 命令层零校验:白名单 / 值域 / 流转全由引擎单点判定
362
+ const r = updateRequirement(data, id, {
363
+ status: options.status,
364
+ title: options.title,
365
+ target_version: options.targetVersion,
366
+ priority: options.priority,
367
+ workflow_mode: options.workflowMode,
368
+ });
369
+ if (!r.ok) {
370
+ const current = getRequirement(data, id)?.status ?? '';
371
+ // done 门专用文案(stage-93 BUG-002):静态表放行 done 而仍被拒 ⇒ 唯一可能是完成门
372
+ const doneGate = options.status === 'done'
373
+ && getAllowedNextStatuses(current).includes('done');
374
+ failCode(lang, r.code, {
375
+ not_found: t('req.err.notFoundTmpl', lang, { id }),
376
+ invalid_field: t('req.err.invalidFieldTmpl', lang, { id }),
377
+ invalid_status: doneGate
378
+ ? t('req.err.doneGateTmpl', lang, { id, current })
379
+ : t('req.err.invalidStatusTmpl', lang, {
380
+ id,
381
+ to: options.status ?? '',
382
+ current,
383
+ targets: getAllowedNextStatuses(current).join(', '),
384
+ }),
385
+ general_mode_conflict: t('req.err.generalModeConflictTmpl', lang, { id }),
386
+ });
387
+ return;
388
+ }
389
+ if (!r.changed) {
390
+ console.log(t('req.set.noopTmpl', lang, { id }));
391
+ return;
392
+ }
393
+ if (!saveOrFail(mgr, lang)) {
394
+ return;
395
+ }
396
+ // stage-90 T2.3:需求级模式下发(先落盘主变更 → 再下发;单点 syncConfigFieldToStages:status.md 字段 + flow.json 冗余同批)
397
+ if (options.workflowMode !== undefined) {
398
+ const linked = Object.entries(data.stages ?? {})
399
+ .filter(([, s]) => (s.requirement_id ?? null) === id)
400
+ .map(([k]) => normalizeStageId(k) ?? k);
401
+ if (linked.length > 0) {
402
+ const synced = mgr.syncConfigFieldToStages('workflow_mode', options.workflowMode, { onlyStages: linked });
403
+ const wrote = synced.filter((s) => s.result === 'updated' || s.result === 'inserted').length;
404
+ console.log(t('req.set.modeSyncedTmpl', lang, { id, mode: options.workflowMode, count: String(wrote) }));
405
+ }
406
+ // 零关联阶段(含 general 门后)→ 仅更新需求字段,零副作用(不输出下发提示)
407
+ }
408
+ console.log(t('req.set.doneTmpl', lang, { id }));
409
+ });
410
+ // ── req link --stage <stageId> (--requirement <id> | --unlink) ──
411
+ req
412
+ .command('link')
413
+ .description('关联需求到阶段 / 解绑(--requirement 与 --unlink 二选一)')
414
+ .requiredOption('--stage <stageId>', '阶段 ID(stage-NN 或 vX.Y.Z-stage-NN)')
415
+ .option('--requirement <id>', '需求 ID(关联用;与 --unlink 互斥)')
416
+ .option('--unlink', '解除该阶段的需求关联')
417
+ .action((options) => {
418
+ const lang = getCliLang(process.cwd());
419
+ const mgr = createManager();
420
+ if (!mgr.isLoaded()) {
421
+ failNoInit(lang, false);
422
+ return;
423
+ }
424
+ const data = mgr.getData();
425
+ // 互斥守卫(argument-shape):both / neither 均拒绝
426
+ const hasRequirement = options.requirement !== undefined;
427
+ const hasUnlink = options.unlink === true;
428
+ if (hasRequirement === hasUnlink) {
429
+ console.error(t('req.link.exclusiveTmpl', lang));
430
+ process.exitCode = 1;
431
+ return;
432
+ }
433
+ // 归一单点(仅此一次;禁自写解析 / 不做双键回退);阶段存在性以引擎为唯一门
434
+ const stageKey = normalizeStageId(options.stage) ?? options.stage;
435
+ let requirementId = null;
436
+ if (hasRequirement) {
437
+ if (!isValidReqId(options.requirement)) {
438
+ console.error(t('req.invalidIdTmpl', lang, { input: options.requirement }));
439
+ process.exitCode = 1;
440
+ return;
441
+ }
442
+ requirementId = options.requirement;
443
+ }
444
+ const r = linkRequirement(data, stageKey, requirementId);
445
+ if (!r.ok) {
446
+ failCode(lang, r.code, {
447
+ stage_not_found: t('req.err.stageNotFoundTmpl', lang, { stage: stageKey }),
448
+ not_found: t('req.err.notFoundTmpl', lang, { id: requirementId ?? '' }),
449
+ invalid_status: t('req.link.terminalTmpl', lang, {
450
+ id: requirementId ?? '',
451
+ status: requirementId ? (getRequirement(data, requirementId)?.status ?? '') : '',
452
+ }),
453
+ already_linked: t('req.err.alreadyLinkedTmpl', lang, { stage: stageKey }),
454
+ general_mode_conflict: t('req.err.generalTargetTmpl', lang, { id: requirementId ?? '' }),
455
+ });
456
+ return;
457
+ }
458
+ if (!r.changed) {
459
+ console.log(t('req.link.noopTmpl', lang, { stage: stageKey }));
460
+ return;
461
+ }
462
+ if (!saveOrFail(mgr, lang)) {
463
+ return;
464
+ }
465
+ // stage-90 T2.3:需求级模式下发(需求携带 workflow_mode 时经单点同步到关联阶段)
466
+ if (hasRequirement && requirementId) {
467
+ const reqLinked = getRequirement(data, requirementId);
468
+ if (reqLinked?.workflow_mode) {
469
+ const synced = mgr.syncConfigFieldToStages('workflow_mode', reqLinked.workflow_mode, { onlyStages: [stageKey] });
470
+ if (synced.some((s) => s.result === 'updated' || s.result === 'inserted')) {
471
+ console.log(t('req.link.modeSyncedTmpl', lang, { stage: stageKey, mode: reqLinked.workflow_mode }));
472
+ }
473
+ }
474
+ }
475
+ console.log(hasRequirement
476
+ ? t('req.link.doneTmpl', lang, { stage: stageKey, id: requirementId ?? '' })
477
+ : t('req.link.unlinkDoneTmpl', lang, { stage: stageKey }));
478
+ });
479
+ // ── req advance <id> --to <status> ──
480
+ req
481
+ .command('advance <id>')
482
+ .description('显式推进需求状态(合法目标由引擎流转表判定;done:非 general 需求须全部关联阶段 done)')
483
+ .requiredOption('--to <status>', '目标状态(合法目标由引擎判定)')
484
+ .action((id, options) => {
485
+ const lang = getCliLang(process.cwd());
486
+ const mgr = createManager();
487
+ if (!mgr.isLoaded()) {
488
+ failNoInit(lang, false);
489
+ return;
490
+ }
491
+ const data = mgr.getData();
492
+ if (!isValidReqId(id)) {
493
+ console.error(t('req.invalidIdTmpl', lang, { input: id }));
494
+ process.exitCode = 1;
495
+ return;
496
+ }
497
+ // 读当前状态仅供成功文案与失败 targets 渲染(纯读,非校验)
498
+ const before = getRequirement(data, id)?.status;
499
+ // 命令层零校验:合法目标 + done 门均在引擎单点
500
+ const r = advanceRequirement(data, id, options.to);
501
+ if (!r.ok) {
502
+ // done 门专用文案(stage-93 BUG-002):静态表放行 done 而仍被拒 ⇒ 唯一可能是完成门
503
+ const doneGate = options.to === 'done'
504
+ && before !== undefined
505
+ && getAllowedNextStatuses(before).includes('done');
506
+ failCode(lang, r.code, {
507
+ not_found: t('req.err.notFoundTmpl', lang, { id }),
508
+ invalid_status: doneGate
509
+ ? t('req.err.doneGateTmpl', lang, { id, current: before ?? '' })
510
+ : t('req.err.invalidStatusTmpl', lang, {
511
+ id,
512
+ to: options.to,
513
+ current: before ?? '',
514
+ targets: before !== undefined ? getAllowedNextStatuses(before).join(', ') : '',
515
+ }),
516
+ });
517
+ return;
518
+ }
519
+ if (!r.changed) {
520
+ console.log(t('req.advance.noopTmpl', lang, { id, to: options.to }));
521
+ return;
522
+ }
523
+ if (!saveOrFail(mgr, lang)) {
524
+ return;
525
+ }
526
+ console.log(t('req.advance.doneTmpl', lang, { id, from: before ?? '', to: options.to }));
527
+ });
528
+ // ── req split <id> --into <title> [--into <title>...] [--json] ──
529
+ req
530
+ .command('split <id>')
531
+ .description('拆分需求为若干子需求(拆分即发布:子需求入 scheduled)')
532
+ .option('--into <title>', '子需求标题(可重复;至少一项)', collectInto, [])
533
+ .option('--json', '以 JSON 输出 { schemaVersion, parent, children }')
534
+ .action((id, options, command) => {
535
+ const lang = getCliLang(process.cwd());
536
+ const asJson = asJsonOf(options, command);
537
+ const mgr = createManager();
538
+ if (!mgr.isLoaded()) {
539
+ failNoInit(lang, asJson);
540
+ return;
541
+ }
542
+ const data = mgr.getData();
543
+ if (!isValidReqId(id)) {
544
+ if (asJson) {
545
+ failJson({ error: 'invalid-argument', field: 'id', value: id });
546
+ }
547
+ else {
548
+ console.error(t('req.invalidIdTmpl', lang, { input: id }));
549
+ process.exitCode = 1;
550
+ }
551
+ return;
552
+ }
553
+ // argument-shape 守卫(非业务校验——对齐 req link 互斥守卫先例)
554
+ const into = options.into ?? [];
555
+ if (into.length === 0) {
556
+ if (asJson) {
557
+ failJson({ error: 'invalid-argument', field: 'into' });
558
+ }
559
+ else {
560
+ console.error(t('req.split.noIntoTmpl', lang));
561
+ process.exitCode = 1;
562
+ }
563
+ return;
564
+ }
565
+ // 命令层零业务校验:父门 / 终态门 / 子项校验 / ID 分配 / 审计全在引擎单点
566
+ const r = splitRequirement(data, id, into.map((title) => ({ title })));
567
+ if (!r.ok) {
568
+ if (asJson) {
569
+ failJson({ error: r.code, id });
570
+ return;
571
+ }
572
+ failCode(lang, r.code, {
573
+ not_found: t('req.err.notFoundTmpl', lang, { id }),
574
+ invalid_status: t('req.split.terminalTmpl', lang, { id, status: getRequirement(data, id)?.status ?? '' }),
575
+ invalid_field: t('req.split.invalidFieldTmpl', lang),
576
+ });
577
+ return;
578
+ }
579
+ // 正文骨架逐条铺(严格直传引擎返回本体,禁浅副本);任一失败 → 不 save(flow.json 零变更——对齐 req add 先例)
580
+ const bodies = [];
581
+ try {
582
+ for (const child of r.children) {
583
+ bodies.push(writeRequirementBodySkeleton(process.cwd(), child));
584
+ }
585
+ }
586
+ catch (err) {
587
+ failGeneric(lang, err);
588
+ return;
589
+ }
590
+ if (!saveOrFail(mgr, lang)) {
591
+ return;
592
+ }
593
+ if (asJson) {
594
+ console.log(JSON.stringify({ schemaVersion: 1, parent: id, children: r.children }, null, 2));
595
+ return;
596
+ }
597
+ console.log(t('req.split.doneTmpl', lang, {
598
+ id, n: String(r.children.length), children: r.children.map((c) => c.id).join(', '),
599
+ }));
600
+ for (let i = 0; i < r.children.length; i += 1) {
601
+ console.log(t('req.split.childTmpl', lang, {
602
+ id: r.children[i].id, title: r.children[i].title, path: bodies[i],
603
+ }));
604
+ }
605
+ });
606
+ // ── req board [--target-version <v>] [--json] ──
607
+ req
608
+ .command('board')
609
+ .description('需求看板(按状态分组;7 组固定顺序)')
610
+ .option('--target-version <version>', '按预估版本过滤')
611
+ .option('--json', '以 JSON 输出 { schemaVersion, groups }')
612
+ .action((options, command) => {
613
+ const lang = getCliLang(process.cwd());
614
+ const asJson = asJsonOf(options, command);
615
+ const mgr = createManager();
616
+ if (!mgr.isLoaded()) {
617
+ failNoInit(lang, asJson);
618
+ return;
619
+ }
620
+ const data = mgr.getData();
621
+ // 只读保证:不 save / 不 appendLog / 零写盘
622
+ const rows = listRequirements(data, { target_version: options.targetVersion });
623
+ // 7 组固定顺序 = 状态值域声明序;恒含空组(域外脏状态不渲染)
624
+ const groups = {};
625
+ for (const s of REQ_STATUSES) {
626
+ groups[s] = rows.filter((r) => r.status === s);
627
+ }
628
+ if (asJson) {
629
+ console.log(JSON.stringify({ schemaVersion: 1, groups }, null, 2));
630
+ return;
631
+ }
632
+ for (const s of REQ_STATUSES) {
633
+ console.log(t('req.board.groupTmpl', lang, { status: s, n: String(groups[s].length) }));
634
+ for (const r of groups[s]) {
635
+ let line = t('req.board.itemTmpl', lang, { id: r.id, title: r.title });
636
+ if (r.owner) {
637
+ line += t('req.board.ownedSuffixTmpl', lang, { owner: r.owner });
638
+ }
639
+ console.log(line);
640
+ }
641
+ }
642
+ });
643
+ // ── req next [--owner <name>] [--limit <n>] [--json] ──
644
+ req
645
+ .command('next')
646
+ .description('推荐下一个可接取需求(scheduled 且未认领;按预估版本升序)')
647
+ .option('--owner <owner>', '认领人视角(含该认领人已认领的 scheduled 需求)')
648
+ .option('--limit <n>', '返回候选条数上限(≥1 的整数;缺省 1)')
649
+ .option('--json', '以 JSON 输出 { schemaVersion, requirement }')
650
+ .action((options, command) => {
651
+ const lang = getCliLang(process.cwd());
652
+ const asJson = asJsonOf(options, command);
653
+ const mgr = createManager();
654
+ if (!mgr.isLoaded()) {
655
+ failNoInit(lang, asJson);
656
+ return;
657
+ }
658
+ const data = mgr.getData();
659
+ // --limit 解析(REV-87-004② 定稿:判定基准 = **解析后数值**,非「是否传参」——显式 1 与缺省同形态)
660
+ const parsed = options.limit === undefined ? 1 : (/^\d+$/.test(options.limit) ? Number(options.limit) : NaN);
661
+ if (!Number.isInteger(parsed) || parsed < 1) {
662
+ if (asJson) {
663
+ failJson({ error: 'invalid-argument', field: 'limit', value: options.limit ?? '' });
664
+ }
665
+ else {
666
+ console.error(t('req.next.limitInvalidTmpl', lang, { value: options.limit ?? '' }));
667
+ process.exitCode = 1;
668
+ }
669
+ return;
670
+ }
671
+ const limit = parsed;
672
+ // 只读保证:不 save / 不 appendLog / 零写盘
673
+ const rows = listRequirements(data, { status: 'scheduled' });
674
+ // 候选:无 --owner = scheduled 且未认领;有 --owner = scheduled 且(未认领 ∨ owner === 该认领人)
675
+ const candidates = rows.filter((r) => !r.owner || (options.owner !== undefined && r.owner === options.owner));
676
+ // 排序(冻结):① 预估版本升序(未指定恒最后)② 创建时间升序 ③ id 升序兜底
677
+ // listRequirements 返回副本数组 → 原地 sort 不污染 flow.json 内存
678
+ candidates.sort((a, b) => {
679
+ const av = a.target_version;
680
+ const bv = b.target_version;
681
+ if (av !== bv) {
682
+ if (av === undefined) {
683
+ return 1;
684
+ }
685
+ if (bv === undefined) {
686
+ return -1;
687
+ }
688
+ if (av < bv) {
689
+ return -1;
690
+ }
691
+ if (av > bv) {
692
+ return 1;
693
+ }
694
+ }
695
+ if (a.created_at !== b.created_at) {
696
+ return a.created_at < b.created_at ? -1 : 1;
697
+ }
698
+ if (a.id !== b.id) {
699
+ return a.id < b.id ? -1 : 1;
700
+ }
701
+ return 0;
702
+ });
703
+ const picked = candidates.slice(0, limit); // 切片只作用于**已排序**结果——候选池与排序冻结语义零变
704
+ const top = picked[0] ?? null;
705
+ if (asJson) {
706
+ // 只增不改:limit === 1 恒既有两键(显式 1 与缺省一致);limit > 1 追加 candidates(既有两键逐字)
707
+ console.log(JSON.stringify(limit === 1
708
+ ? { schemaVersion: 1, requirement: top }
709
+ : { schemaVersion: 1, requirement: top, candidates: picked }, null, 2));
710
+ return;
711
+ }
712
+ if (!top) {
713
+ console.log(t('req.next.empty', lang));
714
+ return;
715
+ }
716
+ if (limit === 1) {
717
+ // 既有单行路径逐字(含 owner 后缀)
718
+ let line = t('req.next.foundTmpl', lang, { id: top.id, status: top.status, title: top.title });
719
+ if (top.owner) {
720
+ line += t('req.next.ownedSuffixTmpl', lang, { owner: top.owner });
721
+ }
722
+ console.log(line);
723
+ return;
724
+ }
725
+ console.log(t('req.next.multiLabel', lang, { n: String(picked.length) }));
726
+ for (const r of picked) {
727
+ let line = t('req.next.itemTmpl', lang, { id: r.id, status: r.status, title: r.title });
728
+ if (r.owner) {
729
+ line += t('req.next.ownedSuffixTmpl', lang, { owner: r.owner });
730
+ }
731
+ console.log(line);
732
+ }
733
+ });
734
+ }
735
+ //# sourceMappingURL=req.js.map