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,16 @@
1
+ import type { RequirementData } from './types.js';
2
+ /** 需求正文目录:{projectPath}/.openfeel/requirements(**不纳入 WORKSPACE_DIRS**,按需 mkdir 幂等创建——与归档目录同口径) */
3
+ export declare function getRequirementsDir(projectPath: string): string;
4
+ /** 正文**存储**相对路径(恒 POSIX 正斜杠,跨平台一致;禁 path.join——Windows 产反斜杠) */
5
+ export declare function getRequirementBodyRelPath(id: string): string;
6
+ /** 正文**文件系统**路径(单点;FS 访问用原生分隔符) */
7
+ export declare function getRequirementBodyPath(projectPath: string, id: string): string;
8
+ /**
9
+ * 机械铺需求正文骨架(人读副本)。
10
+ * - mkdir 幂等;atomicWriteFileSync(backup 缺省 false)
11
+ * - 目标文件已存在 → **no-op 不覆盖**(保留人工补充内容)
12
+ * - 恒将 req.body_path 设为本函数单点派生的 POSIX 相对路径(传入须为 addRequirement 返回的本体)
13
+ * @returns 目标文件的绝对路径
14
+ */
15
+ export declare function writeRequirementBodySkeleton(projectPath: string, req: RequirementData): string;
16
+ //# sourceMappingURL=body-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-file.d.ts","sourceRoot":"","sources":["../../../src/core/req/body-file.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,gEAAgE;AAChE,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,mCAAmC;AACnC,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE9E;AA8CD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,MAAM,CAQ9F"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 需求正文文件原语 — 正文路径单点与骨架机械铺
3
+ *
4
+ * 职责:`.openfeel/requirements/REQ-NNN.md` 正文文件(人读副本)的路径派生与骨架写入。
5
+ * 正交声明:路径全由 `projectPath` 参数传入,零全局路径依赖;不依赖归档层 / 不引用工作流模式字段。
6
+ * 唯一 IO 点:骨架经 `atomicWriteFileSync` 落盘(目录按需 mkdir 幂等创建)。
7
+ */
8
+ import { join } from 'node:path';
9
+ import { existsSync, mkdirSync } from 'node:fs';
10
+ import { atomicWriteFileSync } from '../fs/atomic-write.js';
11
+ /** 需求正文目录:{projectPath}/.openfeel/requirements(**不纳入 WORKSPACE_DIRS**,按需 mkdir 幂等创建——与归档目录同口径) */
12
+ export function getRequirementsDir(projectPath) {
13
+ return join(projectPath, '.openfeel', 'requirements');
14
+ }
15
+ /** 正文**存储**相对路径(恒 POSIX 正斜杠,跨平台一致;禁 path.join——Windows 产反斜杠) */
16
+ export function getRequirementBodyRelPath(id) {
17
+ return `.openfeel/requirements/${id}.md`;
18
+ }
19
+ /** 正文**文件系统**路径(单点;FS 访问用原生分隔符) */
20
+ export function getRequirementBodyPath(projectPath, id) {
21
+ return join(getRequirementsDir(projectPath), `${id}.md`);
22
+ }
23
+ /**
24
+ * 渲染需求正文骨架(裁定 6 模板逐字)。
25
+ * 状态取当前值(参数化:req add 恒 draft;req split 子需求初值 scheduled);`target_version` 缺省渲染为 `(未指定)`。
26
+ */
27
+ function renderRequirementBodySkeleton(req) {
28
+ const targetVersion = req.target_version !== undefined && req.target_version !== ''
29
+ ? req.target_version
30
+ : '(未指定)';
31
+ return [
32
+ `# ${req.id}:${req.title}`,
33
+ '',
34
+ '> 本文件为需求正文(人读副本,由 openfeel req 机械铺骨架)。**机读权威恒为 `.openfeel/flow.json` 的 `requirements` 条目**;两者不一致时以 flow.json 为准。下方状态为创建时快照,不追写实时状态。',
35
+ '',
36
+ '## 状态',
37
+ '',
38
+ `${req.status}(创建时快照;以 flow.json 为准)`,
39
+ '',
40
+ '## 摘要',
41
+ '',
42
+ '(待补充)',
43
+ '',
44
+ '## 目标与验收',
45
+ '',
46
+ '(待补充)',
47
+ '',
48
+ '## 预估版本',
49
+ '',
50
+ targetVersion,
51
+ '',
52
+ '## 父需求',
53
+ '',
54
+ req.parent_id ?? '(无)',
55
+ '',
56
+ '## 关联阶段',
57
+ '',
58
+ '(以 flow.json 的 `stages[*].requirement_id` 为准;此处可人工补充说明)',
59
+ '',
60
+ '## 变更记录',
61
+ '',
62
+ `- ${req.created_at} 创建(${req.id},初始状态 ${req.status})`,
63
+ '',
64
+ ].join('\n');
65
+ }
66
+ /**
67
+ * 机械铺需求正文骨架(人读副本)。
68
+ * - mkdir 幂等;atomicWriteFileSync(backup 缺省 false)
69
+ * - 目标文件已存在 → **no-op 不覆盖**(保留人工补充内容)
70
+ * - 恒将 req.body_path 设为本函数单点派生的 POSIX 相对路径(传入须为 addRequirement 返回的本体)
71
+ * @returns 目标文件的绝对路径
72
+ */
73
+ export function writeRequirementBodySkeleton(projectPath, req) {
74
+ const dir = getRequirementsDir(projectPath);
75
+ mkdirSync(dir, { recursive: true }); // 幂等
76
+ const filePath = getRequirementBodyPath(projectPath, req.id);
77
+ req.body_path = getRequirementBodyRelPath(req.id); // 单点赋值(no-op 路径亦保证一致)
78
+ if (existsSync(filePath)) {
79
+ return filePath;
80
+ } // 已存在 → 不覆盖
81
+ atomicWriteFileSync(filePath, renderRequirementBodySkeleton(req));
82
+ return filePath;
83
+ }
84
+ //# sourceMappingURL=body-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-file.js","sourceRoot":"","sources":["../../../src/core/req/body-file.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG5D,kGAAkG;AAClG,MAAM,UAAU,kBAAkB,CAAC,WAAmB;IACpD,OAAO,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;AACxD,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,yBAAyB,CAAC,EAAU;IAClD,OAAO,0BAA0B,EAAE,KAAK,CAAC;AAC3C,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,sBAAsB,CAAC,WAAmB,EAAE,EAAU;IACpE,OAAO,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,SAAS,6BAA6B,CAAC,GAAoB;IACzD,MAAM,aAAa,GAAG,GAAG,CAAC,cAAc,KAAK,SAAS,IAAI,GAAG,CAAC,cAAc,KAAK,EAAE;QACjF,CAAC,CAAC,GAAG,CAAC,cAAc;QACpB,CAAC,CAAC,OAAO,CAAC;IACZ,OAAO;QACL,KAAK,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,KAAK,EAAE;QAC1B,EAAE;QACF,qIAAqI;QACrI,EAAE;QACF,OAAO;QACP,EAAE;QACF,GAAG,GAAG,CAAC,MAAM,wBAAwB;QACrC,EAAE;QACF,OAAO;QACP,EAAE;QACF,OAAO;QACP,EAAE;QACF,UAAU;QACV,EAAE;QACF,OAAO;QACP,EAAE;QACF,SAAS;QACT,EAAE;QACF,aAAa;QACb,EAAE;QACF,QAAQ;QACR,EAAE;QACF,GAAG,CAAC,SAAS,IAAI,KAAK;QACtB,EAAE;QACF,SAAS;QACT,EAAE;QACF,yDAAyD;QACzD,EAAE;QACF,SAAS;QACT,EAAE;QACF,KAAK,GAAG,CAAC,UAAU,OAAO,GAAG,CAAC,EAAE,SAAS,GAAG,CAAC,MAAM,GAAG;QACtD,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAAC,WAAmB,EAAE,GAAoB;IACpF,MAAM,GAAG,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;IAC5C,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAuB,KAAK;IAChE,MAAM,QAAQ,GAAG,sBAAsB,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAC7D,GAAG,CAAC,SAAS,GAAG,yBAAyB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAS,sBAAsB;IACjF,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAC,OAAO,QAAQ,CAAC;IAAC,CAAC,CAAa,YAAY;IACvE,mBAAmB,CAAC,QAAQ,EAAE,6BAA6B,CAAC,GAAG,CAAC,CAAC,CAAC;IAClE,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * 需求原语 — 需求层纯内存操作的单一入口
3
+ *
4
+ * 职责:需求读取(读侧宽容 + 浅副本)、ID 分配、写路径原语(add / claim / release / update / advance / link)
5
+ * 与共用谓词(终态判定 / 全部关联阶段 done)。全部为**纯内存**操作 + 审计内嵌(`data.log`),
6
+ * 零独立写盘点——落盘恒走调用方(命令层)的 `FlowManager` 落盘方法。
7
+ * 边界声明:不依赖归档层;模式属性(workflow_mode?)为调度属性——本层只做「值域校验 + 载体制门」,
8
+ * 解析 / 下发单点在 FlowManager;不参与聚合判定。
9
+ */
10
+ import type { FlowData } from '../flow-manager.js';
11
+ import { type WorkflowMode } from '../config.js';
12
+ import { type RequirementData, type RequirementPriority, type RequirementStatus } from './types.js';
13
+ /** 需求原语错误码(冻结;命令层据此映射 exit 非 0 零写盘) */
14
+ export type RequirementErrorCode = 'not_found' | 'stage_not_found' | 'already_claimed' | 'already_linked' | 'invalid_status' | 'invalid_field' | 'general_mode_conflict';
15
+ /** 写路径原语结果(ok=false 时携错误码;命令层映射 exit 非 0 零写盘) */
16
+ export type RequirementOpResult = {
17
+ ok: true;
18
+ changed: boolean;
19
+ } | {
20
+ ok: false;
21
+ code: RequirementErrorCode;
22
+ };
23
+ /** 拆分原语结果(成功携子需求**本体**列表;失败携错误码——**业务分流范式**,REV-87-002 定稿) */
24
+ export type RequirementSplitResult = {
25
+ ok: true;
26
+ children: RequirementData[];
27
+ } | {
28
+ ok: false;
29
+ code: RequirementErrorCode;
30
+ };
31
+ /** 全量需求(读侧宽容:缺失/非数组 → [];数组 + 元素浅副本,防外部直改内存) */
32
+ export declare function getRequirements(data: FlowData): RequirementData[];
33
+ /** 按 id 查询(未命中 null;返回浅副本) */
34
+ export declare function getRequirement(data: FlowData, id: string): RequirementData | null;
35
+ /** 过滤查询(status 精确 / target_version 精确 / owner 精确;多条件 AND;API 面命名统一 target_version——REV-81-012) */
36
+ export declare function listRequirements(data: FlowData, filter?: {
37
+ status?: RequirementStatus;
38
+ target_version?: string;
39
+ owner?: string;
40
+ }): RequirementData[];
41
+ /** 下一个需求 ID:现存 id 数字部分 max+1(不回收;异常 id 跳过不崩) */
42
+ export declare function nextRequirementId(data: FlowData): string;
43
+ /** 终态判定(done/cancelled/rejected;claim / link 门共用) */
44
+ export declare function isTerminalRequirementStatus(status: RequirementStatus): boolean;
45
+ /** 裁定 4:全部关联阶段 done(零关联 → false,空集守卫) */
46
+ export declare function allLinkedStagesDone(data: FlowData, requirementId: string): boolean;
47
+ /**
48
+ * 完成门判据(BUG-002 单点收口;裁定 4 扩展为两路——stage-93):
49
+ * 路 1(不变):非 general 需求 → allLinkedStagesDone(有阶段且全 done;零关联恒 false);
50
+ * 路 2(新增):general 需求 → 载体制对偶完成路径(显式完成直接放行;general 恒零关联,
51
+ * 由双向载体制门保证——updateRequirement / linkRequirement)。
52
+ * 判据来源 = 需求自身 workflow_mode 字段直判(与载体制门同源;禁解析级联——解析链仅作用于阶段侧调度)。
53
+ * 防御:general 且有链接 = 载体制门不可达的数据损坏态 → 从严回落路 1(不因模式字段获得额外豁免)。
54
+ */
55
+ export declare function canCompleteRequirement(data: FlowData, req: RequirementData): boolean;
56
+ /**
57
+ * 新建需求(恒 `status: 'draft'`;读侧宽容写路径;返回**本体**供调用方继续设 body_path)。
58
+ * 非法入参(空 title / 非法 priority / 空 target_version)属构造期契约违反 → 抛错(唯一抛错原语,REV-81-015)。
59
+ * REV-81-018 登记:add 构造期抛错 / update 运行期错误码为接受差异(与命令层分流矩阵同口径,REV-82-002);stage-81 冻结面不统一。
60
+ */
61
+ export declare function addRequirement(data: FlowData, input: {
62
+ title: string;
63
+ target_version?: string;
64
+ priority?: RequirementPriority;
65
+ workflow_mode?: WorkflowMode;
66
+ }): RequirementData;
67
+ /**
68
+ * 拆分需求为若干子需求(T1 / Q4 定稿;纯内存 + 单条审计,落盘恒由调用方 save())。
69
+ * 门(顺序固定;任一失败 → **零变更零审计**):
70
+ * ① children 非数组 / 空 → invalid_field(argument-shape)
71
+ * ② 父不存在 → not_found
72
+ * ③ 父终态 → invalid_status(与 claim / link 同款终态门)
73
+ * ④ 逐子项标题校验(trim 后空串)→ invalid_field
74
+ * —— **校验内容**对齐 add 构造期口径,**错误范式**为运行期错误码(REV-87-002 定稿:
75
+ * split 不复用 add 的抛错路径,自持校验循环,与 update 同族——命令层 failCode 分流零适配)。
76
+ * 构造(校验全通过后单趟完成,循环内零可失败语句):
77
+ * - 顺序分配 ID:第 N 子 = 当前 max + N(经 staged 容器复用 nextRequirementId **单源派生**——不新写解析);
78
+ * - parent_id = 父 ID;status = SPLIT_CHILD_STATUS;created_at / updated_at 同批;
79
+ * - **一次性并入** data.requirements(半成品不可达——全成或零变更);
80
+ * - 审计恰一条 requirement_split(detail {parentId, children: [ids]}——对齐 requirement_* 族命名);
81
+ * - 父需求**零副作用**(状态不变 / 不刷 updated_at / 不写父侧字段——父流转由 feel 显式执行)。
82
+ * 环检测:不写——子 ID 恒为新建 max+1、方向恒 `child → parent`,结构上无环。
83
+ * 返回子需求**本体**列表(供调用方逐条铺正文,对齐 addRequirement 返回本体先例;禁浅副本路径)。
84
+ */
85
+ export declare function splitRequirement(data: FlowData, parentId: string, children: Array<{
86
+ title: string;
87
+ }>): RequirementSplitResult;
88
+ /** 认领需求(占用校验;同 owner 幂等;终态拒绝;他人占用零变更零审计) */
89
+ export declare function claimRequirement(data: FlowData, id: string, owner: string): RequirementOpResult;
90
+ /** 释放认领(裁定 5:无占用者 = no-op 成功——不刷 updated_at / 不写审计;不设状态门) */
91
+ export declare function releaseRequirement(data: FlowData, id: string): RequirementOpResult;
92
+ /**
93
+ * 当前状态的合法目标列表(只读纯函数;领域外状态 → [])。
94
+ * 命令层「列合法目标」的唯一来源——禁第二份流转表(REV-82-003);返回副本,防调用方直改内部表。
95
+ */
96
+ export declare function getAllowedNextStatuses(status: RequirementStatus): RequirementStatus[];
97
+ /**
98
+ * 更新需求(patch 白名单;全部维度先校验后落地——校验失败整 patch 零变更)。
99
+ * 恒 raw find 改 `data.requirements` 本体(禁经 getRequirement 的浅副本——副本陷阱,REV-81-011)。
100
+ */
101
+ export declare function updateRequirement(data: FlowData, id: string, patch: {
102
+ status?: RequirementStatus;
103
+ title?: string;
104
+ target_version?: string;
105
+ priority?: RequirementPriority;
106
+ workflow_mode?: WorkflowMode;
107
+ }): RequirementOpResult;
108
+ /** 显式人工推进(裁定 M);语义与 update({status}) 完全一致(同表同门同审计,零第二套) */
109
+ export declare function advanceRequirement(data: FlowData, id: string, to: RequirementStatus): RequirementOpResult;
110
+ /**
111
+ * 阶段关联需求(stage 侧写点;`null` = 解绑)。
112
+ * 终态需求拒绝关联、他人已关联阶段拒绝重绑(须先解绑);
113
+ * 不刷新 requirement.updated_at(关联写点落在 stage 记录,需求对象零变更)。
114
+ */
115
+ export declare function linkRequirement(data: FlowData, stageId: string, requirementId: string | null): RequirementOpResult;
116
+ //# sourceMappingURL=requirement-ops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requirement-ops.d.ts","sourceRoot":"","sources":["../../../src/core/req/requirement-ops.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,395 @@
1
+ import { WORKFLOW_MODES } from '../config.js'; // 值域单源(运行时值导入,禁就地三值字面量)
2
+ import { REQ_ID_PATTERN, REQ_PRIORITIES, REQ_STATUSES, } from './types.js';
3
+ /** 追加需求审计日志(纯内存;save 由调用方承担) */
4
+ function appendRequirementLog(data, action, detail) {
5
+ data.log.push({ time: new Date().toISOString(), agent: 'cli', action, detail });
6
+ }
7
+ // ═══ 读取与分配(读侧宽容 + 浅副本,裁定 2/8)═══
8
+ /** 全量需求(读侧宽容:缺失/非数组 → [];数组 + 元素浅副本,防外部直改内存) */
9
+ export function getRequirements(data) {
10
+ const list = data.requirements;
11
+ return Array.isArray(list) ? list.map((r) => ({ ...r })) : [];
12
+ }
13
+ /** 按 id 查询(未命中 null;返回浅副本) */
14
+ export function getRequirement(data, id) {
15
+ const found = (Array.isArray(data.requirements) ? data.requirements : []).find((r) => r?.id === id);
16
+ return found ? { ...found } : null;
17
+ }
18
+ /** 过滤查询(status 精确 / target_version 精确 / owner 精确;多条件 AND;API 面命名统一 target_version——REV-81-012) */
19
+ export function listRequirements(data, filter) {
20
+ let result = getRequirements(data);
21
+ if (filter?.status !== undefined) {
22
+ result = result.filter((r) => r.status === filter.status);
23
+ }
24
+ if (filter?.target_version !== undefined) {
25
+ result = result.filter((r) => r.target_version === filter.target_version);
26
+ }
27
+ if (filter?.owner !== undefined) {
28
+ result = result.filter((r) => r.owner === filter.owner);
29
+ }
30
+ return result;
31
+ }
32
+ /** 下一个需求 ID:现存 id 数字部分 max+1(不回收;异常 id 跳过不崩) */
33
+ export function nextRequirementId(data) {
34
+ let max = 0;
35
+ for (const r of getRequirements(data)) {
36
+ const m = REQ_ID_PATTERN.exec(typeof r?.id === 'string' ? r.id : '');
37
+ if (!m) {
38
+ continue;
39
+ } // 异常 id 跳过(不崩)
40
+ const n = Number.parseInt(m[1], 10);
41
+ if (Number.isFinite(n) && n > max) {
42
+ max = n;
43
+ }
44
+ }
45
+ return `REQ-${String(max + 1).padStart(3, '0')}`;
46
+ }
47
+ /** 终态判定(done/cancelled/rejected;claim / link 门共用) */
48
+ export function isTerminalRequirementStatus(status) {
49
+ return status === 'done' || status === 'cancelled' || status === 'rejected';
50
+ }
51
+ /** 裁定 4:全部关联阶段 done(零关联 → false,空集守卫) */
52
+ export function allLinkedStagesDone(data, requirementId) {
53
+ const linked = Object.values(data.stages ?? {}).filter((s) => (s.requirement_id ?? null) === requirementId);
54
+ return linked.length > 0 && linked.every((s) => s.phase === 'done');
55
+ }
56
+ /**
57
+ * 完成门判据(BUG-002 单点收口;裁定 4 扩展为两路——stage-93):
58
+ * 路 1(不变):非 general 需求 → allLinkedStagesDone(有阶段且全 done;零关联恒 false);
59
+ * 路 2(新增):general 需求 → 载体制对偶完成路径(显式完成直接放行;general 恒零关联,
60
+ * 由双向载体制门保证——updateRequirement / linkRequirement)。
61
+ * 判据来源 = 需求自身 workflow_mode 字段直判(与载体制门同源;禁解析级联——解析链仅作用于阶段侧调度)。
62
+ * 防御:general 且有链接 = 载体制门不可达的数据损坏态 → 从严回落路 1(不因模式字段获得额外豁免)。
63
+ */
64
+ export function canCompleteRequirement(data, req) {
65
+ if (req.workflow_mode === 'general') {
66
+ const hasLinked = Object.values(data.stages ?? {}).some((s) => (s.requirement_id ?? null) === req.id);
67
+ if (!hasLinked) {
68
+ return true;
69
+ }
70
+ }
71
+ return allLinkedStagesDone(data, req.id);
72
+ }
73
+ // ═══ 写路径原语 ═══
74
+ /**
75
+ * 新建需求(恒 `status: 'draft'`;读侧宽容写路径;返回**本体**供调用方继续设 body_path)。
76
+ * 非法入参(空 title / 非法 priority / 空 target_version)属构造期契约违反 → 抛错(唯一抛错原语,REV-81-015)。
77
+ * REV-81-018 登记:add 构造期抛错 / update 运行期错误码为接受差异(与命令层分流矩阵同口径,REV-82-002);stage-81 冻结面不统一。
78
+ */
79
+ export function addRequirement(data, input) {
80
+ const title = typeof input?.title === 'string' ? input.title.trim() : '';
81
+ if (title === '') {
82
+ throw new Error('需求标题不能为空');
83
+ } // 非法入参:编程错误抛错(非业务冲突)
84
+ if (input.priority !== undefined && !REQ_PRIORITIES.includes(input.priority)) {
85
+ throw new Error(`需求优先级非法:${String(input.priority)}`); // 值域单点 = REQ_PRIORITIES
86
+ }
87
+ if (input.workflow_mode !== undefined && !WORKFLOW_MODES.includes(input.workflow_mode)) {
88
+ throw new Error(`需求工作流模式非法:${String(input.workflow_mode)}`); // 值域单点 = WORKFLOW_MODES
89
+ }
90
+ // 命名统一(REV-81-012):API 面恒 target_version(与存储字段同名,零映射层)
91
+ if (input.target_version !== undefined && String(input.target_version).trim() === '') {
92
+ throw new Error('预估版本不能为空串');
93
+ }
94
+ // 非数组(损坏数据)视同空集重建——与其余原语「非数组 ≡ 空集」宽容口径统一(req/BUG-001 = REV-81-019 单点收口);改自 ??=(??= 不替换非数组真值 → push 抛 TypeError)
95
+ data.requirements = Array.isArray(data.requirements) ? data.requirements : [];
96
+ const now = new Date().toISOString();
97
+ const req = {
98
+ id: nextRequirementId(data), // 须在 push 之前分配
99
+ title,
100
+ status: 'draft', // 新建即 draft(REV-81-005)
101
+ created_at: now,
102
+ updated_at: now,
103
+ };
104
+ if (input.target_version !== undefined) {
105
+ req.target_version = String(input.target_version).trim();
106
+ }
107
+ if (input.priority !== undefined) {
108
+ req.priority = input.priority;
109
+ }
110
+ if (input.workflow_mode !== undefined) {
111
+ req.workflow_mode = input.workflow_mode;
112
+ }
113
+ data.requirements.push(req);
114
+ appendRequirementLog(data, 'requirement_add', { id: req.id, title: req.title });
115
+ return req; // 本体(调用方继续设 body_path)
116
+ }
117
+ /** 子需求初值状态:拆分即发布(Q11 定稿——子需求直接入 `req next` 候选池,达成「并行接取就绪」) */
118
+ const SPLIT_CHILD_STATUS = 'scheduled';
119
+ /**
120
+ * 拆分需求为若干子需求(T1 / Q4 定稿;纯内存 + 单条审计,落盘恒由调用方 save())。
121
+ * 门(顺序固定;任一失败 → **零变更零审计**):
122
+ * ① children 非数组 / 空 → invalid_field(argument-shape)
123
+ * ② 父不存在 → not_found
124
+ * ③ 父终态 → invalid_status(与 claim / link 同款终态门)
125
+ * ④ 逐子项标题校验(trim 后空串)→ invalid_field
126
+ * —— **校验内容**对齐 add 构造期口径,**错误范式**为运行期错误码(REV-87-002 定稿:
127
+ * split 不复用 add 的抛错路径,自持校验循环,与 update 同族——命令层 failCode 分流零适配)。
128
+ * 构造(校验全通过后单趟完成,循环内零可失败语句):
129
+ * - 顺序分配 ID:第 N 子 = 当前 max + N(经 staged 容器复用 nextRequirementId **单源派生**——不新写解析);
130
+ * - parent_id = 父 ID;status = SPLIT_CHILD_STATUS;created_at / updated_at 同批;
131
+ * - **一次性并入** data.requirements(半成品不可达——全成或零变更);
132
+ * - 审计恰一条 requirement_split(detail {parentId, children: [ids]}——对齐 requirement_* 族命名);
133
+ * - 父需求**零副作用**(状态不变 / 不刷 updated_at / 不写父侧字段——父流转由 feel 显式执行)。
134
+ * 环检测:不写——子 ID 恒为新建 max+1、方向恒 `child → parent`,结构上无环。
135
+ * 返回子需求**本体**列表(供调用方逐条铺正文,对齐 addRequirement 返回本体先例;禁浅副本路径)。
136
+ */
137
+ export function splitRequirement(data, parentId, children) {
138
+ if (!Array.isArray(children) || children.length === 0) {
139
+ return { ok: false, code: 'invalid_field' };
140
+ }
141
+ const parent = (Array.isArray(data.requirements) ? data.requirements : []).find((r) => r?.id === parentId);
142
+ if (!parent) {
143
+ return { ok: false, code: 'not_found' };
144
+ }
145
+ if (isTerminalRequirementStatus(parent.status)) {
146
+ return { ok: false, code: 'invalid_status' };
147
+ }
148
+ const titles = [];
149
+ for (const c of children) {
150
+ const title = typeof c?.title === 'string' ? c.title.trim() : '';
151
+ if (title === '') {
152
+ return { ok: false, code: 'invalid_field' };
153
+ }
154
+ titles.push(title);
155
+ }
156
+ // staged 容器承载「当前 max」:复用 nextRequirementId 单源派生 → 顺序分配且不重解析
157
+ const list = Array.isArray(data.requirements) ? [...data.requirements] : [];
158
+ const staged = { ...data, requirements: list };
159
+ const now = new Date().toISOString();
160
+ const created = [];
161
+ for (const title of titles) {
162
+ const child = {
163
+ id: nextRequirementId(staged),
164
+ title,
165
+ status: SPLIT_CHILD_STATUS,
166
+ parent_id: parentId,
167
+ created_at: now,
168
+ updated_at: now,
169
+ };
170
+ list.push(child);
171
+ created.push(child);
172
+ }
173
+ data.requirements = list;
174
+ appendRequirementLog(data, 'requirement_split', { parentId, children: created.map((c) => c.id) });
175
+ return { ok: true, children: created };
176
+ }
177
+ /** 认领需求(占用校验;同 owner 幂等;终态拒绝;他人占用零变更零审计) */
178
+ export function claimRequirement(data, id, owner) {
179
+ const name = typeof owner === 'string' ? owner.trim() : '';
180
+ if (name === '') {
181
+ return { ok: false, code: 'invalid_field' };
182
+ }
183
+ const req = (Array.isArray(data.requirements) ? data.requirements : []).find((r) => r?.id === id);
184
+ if (!req) {
185
+ return { ok: false, code: 'not_found' };
186
+ }
187
+ if (isTerminalRequirementStatus(req.status)) {
188
+ return { ok: false, code: 'invalid_status' };
189
+ }
190
+ if (req.owner && req.owner !== name) {
191
+ return { ok: false, code: 'already_claimed' };
192
+ } // 数据零变更
193
+ if (req.owner === name) {
194
+ return { ok: true, changed: false };
195
+ } // 同 owner 幂等
196
+ req.owner = name;
197
+ req.claimed_at = new Date().toISOString();
198
+ req.updated_at = req.claimed_at;
199
+ appendRequirementLog(data, 'requirement_claim', { id, owner: name });
200
+ return { ok: true, changed: true };
201
+ }
202
+ /** 释放认领(裁定 5:无占用者 = no-op 成功——不刷 updated_at / 不写审计;不设状态门) */
203
+ export function releaseRequirement(data, id) {
204
+ const req = (Array.isArray(data.requirements) ? data.requirements : []).find((r) => r?.id === id);
205
+ if (!req) {
206
+ return { ok: false, code: 'not_found' };
207
+ }
208
+ if (!req.owner) {
209
+ return { ok: true, changed: false };
210
+ } // 无占用者:no-op 成功(不刷 updated_at / 不写审计)
211
+ const prevOwner = req.owner;
212
+ delete req.owner;
213
+ delete req.claimed_at;
214
+ req.updated_at = new Date().toISOString();
215
+ appendRequirementLog(data, 'requirement_release', { id, owner: prevOwner });
216
+ return { ok: true, changed: true };
217
+ }
218
+ /** 显式流转表(裁定 3;终态不再流转)——状态流转单点 */
219
+ const ALLOWED_TRANSITIONS = {
220
+ draft: ['scheduled', 'blocked', 'cancelled', 'rejected'],
221
+ scheduled: ['in_progress', 'blocked', 'cancelled', 'rejected'],
222
+ in_progress: ['done', 'blocked', 'cancelled', 'rejected'],
223
+ blocked: ['scheduled', 'in_progress', 'cancelled', 'rejected'],
224
+ done: [], cancelled: [], rejected: [],
225
+ };
226
+ /**
227
+ * 当前状态的合法目标列表(只读纯函数;领域外状态 → [])。
228
+ * 命令层「列合法目标」的唯一来源——禁第二份流转表(REV-82-003);返回副本,防调用方直改内部表。
229
+ */
230
+ export function getAllowedNextStatuses(status) {
231
+ return [...(ALLOWED_TRANSITIONS[status] ?? [])];
232
+ }
233
+ /** 状态变更应用(唯一实现):返回 false = invalid_status(含 done 门不满足) */
234
+ function applyStatusChange(data, req, to) {
235
+ if (!REQ_STATUSES.includes(to)) {
236
+ return false;
237
+ }
238
+ if (to === req.status) {
239
+ return false;
240
+ } // 同值由调用方按幂等 no-op 处理
241
+ // 域外 status(存量脏数据 / 人工篡改)→ 查表得 undefined,守卫后与「表外组合」同语义,不抛 TypeError(REV-81-016)
242
+ const allowed = ALLOWED_TRANSITIONS[req.status];
243
+ if (!allowed || !allowed.includes(to)) {
244
+ return false;
245
+ }
246
+ // 裁定 4 扩展(stage-93):completion 判据单点(canCompleteRequirement),不再直呼路 1
247
+ if (to === 'done' && !canCompleteRequirement(data, req)) {
248
+ return false;
249
+ }
250
+ const from = req.status;
251
+ req.status = to;
252
+ req.updated_at = new Date().toISOString(); // 状态维度刷新(单次赋值纪律,REV-81-013)
253
+ appendRequirementLog(data, 'requirement_status', { id: req.id, from, to });
254
+ return true;
255
+ }
256
+ /** 字段白名单(= RequirementData 可变字段名子集) */
257
+ const PATCH_WHITELIST = ['status', 'title', 'target_version', 'priority', 'workflow_mode'];
258
+ /**
259
+ * 更新需求(patch 白名单;全部维度先校验后落地——校验失败整 patch 零变更)。
260
+ * 恒 raw find 改 `data.requirements` 本体(禁经 getRequirement 的浅副本——副本陷阱,REV-81-011)。
261
+ */
262
+ export function updateRequirement(data, id, patch) {
263
+ // ① 内联 raw find(与 claim / release 同款)
264
+ const req = (Array.isArray(data.requirements) ? data.requirements : []).find((r) => r?.id === id);
265
+ if (!req) {
266
+ return { ok: false, code: 'not_found' };
267
+ }
268
+ const p = patch ?? {};
269
+ const keys = Object.entries(p).filter(([, v]) => v !== undefined).map(([k]) => k);
270
+ // ② 白名单外键(值非 undefined 才计)
271
+ for (const k of keys) {
272
+ if (!PATCH_WHITELIST.includes(k)) {
273
+ return { ok: false, code: 'invalid_field' };
274
+ }
275
+ }
276
+ // ③ 全空 patch(或全 undefined 值)
277
+ if (keys.length === 0) {
278
+ return { ok: true, changed: false };
279
+ }
280
+ // ④ 值校验(全部维度先校验,失败即整 patch 零变更)
281
+ // REV-81-018 登记:add 构造期抛错 / update 运行期错误码为接受差异(与命令层分流矩阵同口径,REV-82-002);stage-81 冻结面不统一。
282
+ if (p.title !== undefined && String(p.title).trim() === '') {
283
+ return { ok: false, code: 'invalid_field' };
284
+ }
285
+ if (p.target_version !== undefined && (typeof p.target_version !== 'string' || p.target_version.trim() === '')) {
286
+ return { ok: false, code: 'invalid_field' };
287
+ }
288
+ if (p.priority !== undefined && !REQ_PRIORITIES.includes(p.priority)) {
289
+ return { ok: false, code: 'invalid_field' };
290
+ }
291
+ if (p.status !== undefined && !REQ_STATUSES.includes(p.status)) {
292
+ return { ok: false, code: 'invalid_status' };
293
+ }
294
+ if (p.workflow_mode !== undefined && !WORKFLOW_MODES.includes(p.workflow_mode)) {
295
+ return { ok: false, code: 'invalid_field' }; // 值域单点 = WORKFLOW_MODES
296
+ }
297
+ // 载体制门(D-90-4):general 仅限**无关联阶段**的需求(有链接阶段 → 拒绝;零变更零审计)
298
+ if (p.workflow_mode === 'general') {
299
+ const hasLinked = Object.values(data.stages ?? {}).some((s) => (s.requirement_id ?? null) === id);
300
+ if (hasLinked) {
301
+ return { ok: false, code: 'general_mode_conflict' };
302
+ }
303
+ }
304
+ // ⑥ 状态维度(= 当前则无变更;≠ 走单点 applyStatusChange)
305
+ let statusChanged = false;
306
+ if (p.status !== undefined && p.status !== req.status) {
307
+ if (!applyStatusChange(data, req, p.status)) {
308
+ return { ok: false, code: 'invalid_status' };
309
+ }
310
+ statusChanged = true;
311
+ }
312
+ // ⑦ 字段维度(逐字段比较后对 raw 本体赋值;仅实际不同才赋值)
313
+ const changedFields = [];
314
+ if (p.title !== undefined) {
315
+ const v = String(p.title).trim();
316
+ if (req.title !== v) {
317
+ req.title = v;
318
+ changedFields.push('title');
319
+ }
320
+ }
321
+ if (p.target_version !== undefined) {
322
+ const v = String(p.target_version).trim();
323
+ if (req.target_version !== v) {
324
+ req.target_version = v;
325
+ changedFields.push('target_version');
326
+ }
327
+ }
328
+ if (p.priority !== undefined) {
329
+ if (req.priority !== p.priority) {
330
+ req.priority = p.priority;
331
+ changedFields.push('priority');
332
+ }
333
+ }
334
+ if (p.workflow_mode !== undefined) {
335
+ if (req.workflow_mode !== p.workflow_mode) {
336
+ req.workflow_mode = p.workflow_mode;
337
+ changedFields.push('workflow_mode');
338
+ }
339
+ }
340
+ // ⑧ updated_at 单次赋值纪律(REV-81-013):字段维度实际变更且状态维度未变时刷新一次;
341
+ // 状态维度已由 applyStatusChange 刷新,两维度并存时不重复赋值
342
+ if (changedFields.length > 0) {
343
+ if (!statusChanged) {
344
+ req.updated_at = new Date().toISOString();
345
+ }
346
+ appendRequirementLog(data, 'requirement_update', { id, fields: changedFields });
347
+ }
348
+ // ⑩
349
+ return { ok: true, changed: statusChanged || changedFields.length > 0 };
350
+ }
351
+ /** 显式人工推进(裁定 M);语义与 update({status}) 完全一致(同表同门同审计,零第二套) */
352
+ export function advanceRequirement(data, id, to) {
353
+ return updateRequirement(data, id, { status: to });
354
+ }
355
+ /**
356
+ * 阶段关联需求(stage 侧写点;`null` = 解绑)。
357
+ * 终态需求拒绝关联、他人已关联阶段拒绝重绑(须先解绑);
358
+ * 不刷新 requirement.updated_at(关联写点落在 stage 记录,需求对象零变更)。
359
+ */
360
+ export function linkRequirement(data, stageId, requirementId) {
361
+ const stage = data.stages?.[stageId];
362
+ if (!stage) {
363
+ return { ok: false, code: 'stage_not_found' };
364
+ } // ① 阶段不存在
365
+ if (requirementId !== null) {
366
+ const req = (Array.isArray(data.requirements) ? data.requirements : []).find((r) => r?.id === requirementId);
367
+ if (!req) {
368
+ return { ok: false, code: 'not_found' };
369
+ } // 需求不存在
370
+ if (isTerminalRequirementStatus(req.status)) {
371
+ return { ok: false, code: 'invalid_status' };
372
+ } // 终态拒绝
373
+ // 载体制门(D-90-4):general(无流水线)需求不接受阶段关联
374
+ if (req.workflow_mode === 'general') {
375
+ return { ok: false, code: 'general_mode_conflict' };
376
+ }
377
+ if ((stage.requirement_id ?? null) === requirementId) {
378
+ return { ok: true, changed: false };
379
+ } // 同值幂等
380
+ if (stage.requirement_id != null) {
381
+ return { ok: false, code: 'already_linked' };
382
+ } // 跨需求重绑
383
+ stage.requirement_id = requirementId;
384
+ appendRequirementLog(data, 'requirement_link', { stage: stageId, requirementId });
385
+ return { ok: true, changed: true };
386
+ }
387
+ // ③ null = 解绑(显式 null,对齐「可空」裁定 D)
388
+ if (stage.requirement_id == null) {
389
+ return { ok: true, changed: false };
390
+ }
391
+ stage.requirement_id = null;
392
+ appendRequirementLog(data, 'requirement_link', { stage: stageId, requirementId: null });
393
+ return { ok: true, changed: true };
394
+ }
395
+ //# sourceMappingURL=requirement-ops.js.map