openfeel 1.1.7 → 1.2.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 (78) hide show
  1. package/README.en.md +13 -5
  2. package/README.md +8 -2
  3. package/README.zh-CN.md +13 -5
  4. package/dist/cli/index.d.ts.map +1 -1
  5. package/dist/cli/index.js +2 -0
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/commands/config.d.ts.map +1 -1
  8. package/dist/commands/config.js +23 -4
  9. package/dist/commands/config.js.map +1 -1
  10. package/dist/commands/flow.d.ts.map +1 -1
  11. package/dist/commands/flow.js +279 -6
  12. package/dist/commands/flow.js.map +1 -1
  13. package/dist/commands/plan.d.ts.map +1 -1
  14. package/dist/commands/plan.js +12 -5
  15. package/dist/commands/plan.js.map +1 -1
  16. package/dist/commands/req.d.ts +14 -0
  17. package/dist/commands/req.d.ts.map +1 -0
  18. package/dist/commands/req.js +562 -0
  19. package/dist/commands/req.js.map +1 -0
  20. package/dist/core/config.js +2 -2
  21. package/dist/core/flow-archive/archive-file.d.ts +35 -0
  22. package/dist/core/flow-archive/archive-file.d.ts.map +1 -0
  23. package/dist/core/flow-archive/archive-file.js +3 -0
  24. package/dist/core/flow-archive/archive-file.js.map +1 -0
  25. package/dist/core/flow-archive/archive-health.d.ts +34 -0
  26. package/dist/core/flow-archive/archive-health.d.ts.map +1 -0
  27. package/dist/core/flow-archive/archive-health.js +76 -0
  28. package/dist/core/flow-archive/archive-health.js.map +1 -0
  29. package/dist/core/flow-archive/archive-ops.d.ts +68 -0
  30. package/dist/core/flow-archive/archive-ops.d.ts.map +1 -0
  31. package/dist/core/flow-archive/archive-ops.js +184 -0
  32. package/dist/core/flow-archive/archive-ops.js.map +1 -0
  33. package/dist/core/flow-archive/grouping.d.ts +58 -0
  34. package/dist/core/flow-archive/grouping.d.ts.map +1 -0
  35. package/dist/core/flow-archive/grouping.js +153 -0
  36. package/dist/core/flow-archive/grouping.js.map +1 -0
  37. package/dist/core/flow-manager.d.ts +7 -0
  38. package/dist/core/flow-manager.d.ts.map +1 -1
  39. package/dist/core/flow-manager.js +54 -13
  40. package/dist/core/flow-manager.js.map +1 -1
  41. package/dist/core/i18n-data/en.d.ts +1 -0
  42. package/dist/core/i18n-data/en.d.ts.map +1 -1
  43. package/dist/core/i18n-data/en.js +134 -0
  44. package/dist/core/i18n-data/en.js.map +1 -1
  45. package/dist/core/i18n-data/zh-CN.d.ts +1 -0
  46. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  47. package/dist/core/i18n-data/zh-CN.js +135 -0
  48. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  49. package/dist/core/i18n.js +4 -4
  50. package/dist/core/i18n.js.map +1 -1
  51. package/dist/core/plan/path.d.ts +17 -0
  52. package/dist/core/plan/path.d.ts.map +1 -1
  53. package/dist/core/plan/path.js +31 -0
  54. package/dist/core/plan/path.js.map +1 -1
  55. package/dist/core/req/aggregate.d.ts +61 -0
  56. package/dist/core/req/aggregate.d.ts.map +1 -0
  57. package/dist/core/req/aggregate.js +134 -0
  58. package/dist/core/req/aggregate.js.map +1 -0
  59. package/dist/core/req/body-file.d.ts +16 -0
  60. package/dist/core/req/body-file.d.ts.map +1 -0
  61. package/dist/core/req/body-file.js +80 -0
  62. package/dist/core/req/body-file.js.map +1 -0
  63. package/dist/core/req/requirement-ops.d.ts +74 -0
  64. package/dist/core/req/requirement-ops.d.ts.map +1 -0
  65. package/dist/core/req/requirement-ops.js +290 -0
  66. package/dist/core/req/requirement-ops.js.map +1 -0
  67. package/dist/core/req/types.d.ts +40 -0
  68. package/dist/core/req/types.d.ts.map +1 -0
  69. package/dist/core/req/types.js +13 -0
  70. package/dist/core/req/types.js.map +1 -0
  71. package/dist/core/setup.js +4 -4
  72. package/dist/core/template-loader.d.ts.map +1 -1
  73. package/dist/core/template-loader.js +305 -12
  74. package/dist/core/template-loader.js.map +1 -1
  75. package/dist/core/update.d.ts.map +1 -1
  76. package/dist/core/update.js +91 -8
  77. package/dist/core/update.js.map +1 -1
  78. package/package.json +1 -1
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 归档一致性健康检查项构造 — 纯函数
3
+ * 职责:构造 `flow health` 的「归档一致性」检查项——
4
+ * a. 归档索引(`meta.archived_versions`)↔ 归档目录清单**双向对账**(恒产生 1 条 pass/warn);
5
+ * b. 活跃集残留(flow.json `stages` 仍含已归档版本阶段;仅非空时产生一条 warn)。
6
+ * 边界:**只读**——读取轻量索引(`getArchivedVersions`)+ 归档目录清单级(`listArchiveVersions`,目录级);
7
+ * **b 项候选分支**(活跃键版本前缀 ∈ 索引)按版本**惰性读一次**匹配快照(`readArchiveFile`,
8
+ * 仅取 `stages` 键集,不读 reviews/log);常规(无候选)零读归档文件;不写盘、不加锁。
9
+ * 契约修订(REV-80-019;落点更正 REV-80-024):原「不读归档文件内容」契约实际落点 =
10
+ * `stage-80/plan.md §二 不变量 4`(:61)+ 大计划 `v1.2.0/plan.md §八 item 4`(:266)/ 裁定 Q(:118),
11
+ * **不在 `§四.4`**(plan.md 措辞按 REV-80-020 移交 planner 修订)。
12
+ * 依赖方向(REV-80-005):对 flow-manager 仅 `import type`(编译期擦除,零运行时依赖边);
13
+ * 本文件仅被 `src/commands/flow.ts` 消费,禁 `flow-manager → flow-archive` 运行时依赖。
14
+ */
15
+ import type { FlowData, HealthCheckItem } from '../flow-manager.js';
16
+ /**
17
+ * 构造归档一致性检查项(a 双向对账 + b 活跃集残留)。
18
+ *
19
+ * - a 项(恒产生 1 条 pass/warn):`archived_versions` ↔ `.openfeel/flow-archive/*.json` 目录清单双向对账;
20
+ * 两侧差集均空 → pass;否则 warn(索引有文件缺 / 文件有索引缺)。
21
+ * - b 项(仅非空时产生 warn,**两段式判据**——REV-80-019):候选 = ① 版本前缀 ∈ `archived_versions`
22
+ * (索引级廉价预筛)**∧** ② 键 ∈ 对应归档快照 `stages` 键集(`readArchiveFile`,键级精确证据)——
23
+ * 「默认版本新建阶段」(键 ∉ 快照)不误报;快照不可读(缺失/损坏 → 读侧宽容 null)→ 不可证实 → 不报
24
+ * (索引/文件不一致由 a 项承担)。
25
+ *
26
+ * @param input.projectPath 项目根路径(扫描目录清单;b 项候选分支读匹配版本快照以取 `stages` 键集)
27
+ * @param input.data 当前 flow.json 数据(只读)
28
+ * @returns HealthCheckItem[];全部项 `status ∈ {'pass','warn'}`(**恒无 fail**)
29
+ */
30
+ export declare function archiveHealthItems(input: {
31
+ projectPath: string;
32
+ data: FlowData;
33
+ }): HealthCheckItem[];
34
+ //# sourceMappingURL=archive-health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-health.d.ts","sourceRoot":"","sources":["../../../src/core/flow-archive/archive-health.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AASpE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG,eAAe,EAAE,CAwDpG"}
@@ -0,0 +1,76 @@
1
+ import { getArchivedVersions, listArchiveVersions, readArchiveFile } from './archive-ops.js';
2
+ import { versionOfStageId } from './grouping.js';
3
+ /** a 项 section 常量(双向对账;与测试断言一致,REV-80-012) */
4
+ const SECTION_INDEX = '归档一致性';
5
+ /** b 项 section 常量(活跃集残留;与测试断言一致,REV-80-012) */
6
+ const SECTION_RESIDUAL = '归档残留';
7
+ /**
8
+ * 构造归档一致性检查项(a 双向对账 + b 活跃集残留)。
9
+ *
10
+ * - a 项(恒产生 1 条 pass/warn):`archived_versions` ↔ `.openfeel/flow-archive/*.json` 目录清单双向对账;
11
+ * 两侧差集均空 → pass;否则 warn(索引有文件缺 / 文件有索引缺)。
12
+ * - b 项(仅非空时产生 warn,**两段式判据**——REV-80-019):候选 = ① 版本前缀 ∈ `archived_versions`
13
+ * (索引级廉价预筛)**∧** ② 键 ∈ 对应归档快照 `stages` 键集(`readArchiveFile`,键级精确证据)——
14
+ * 「默认版本新建阶段」(键 ∉ 快照)不误报;快照不可读(缺失/损坏 → 读侧宽容 null)→ 不可证实 → 不报
15
+ * (索引/文件不一致由 a 项承担)。
16
+ *
17
+ * @param input.projectPath 项目根路径(扫描目录清单;b 项候选分支读匹配版本快照以取 `stages` 键集)
18
+ * @param input.data 当前 flow.json 数据(只读)
19
+ * @returns HealthCheckItem[];全部项 `status ∈ {'pass','warn'}`(**恒无 fail**)
20
+ */
21
+ export function archiveHealthItems(input) {
22
+ const items = [];
23
+ const { projectPath, data } = input;
24
+ // a. 双向对账(索引 ↔ 文件目录清单)
25
+ const indexed = getArchivedVersions(data); // 快照副本
26
+ const files = listArchiveVersions(projectPath); // 目录清单级、字典序
27
+ const indexedSet = new Set(indexed);
28
+ const filesSet = new Set(files);
29
+ const missingFiles = indexed.filter((v) => !filesSet.has(v)); // 索引有、文件缺
30
+ const missingIndex = files.filter((v) => !indexedSet.has(v)); // 文件有、索引缺
31
+ if (missingFiles.length === 0 && missingIndex.length === 0) {
32
+ items.push({
33
+ section: SECTION_INDEX,
34
+ status: 'pass',
35
+ message: `归档索引与归档文件一致(${indexed.length} 个版本)`,
36
+ });
37
+ }
38
+ else {
39
+ // 两侧差集任一非空 → warn(展示各≤5 条)
40
+ items.push({
41
+ section: SECTION_INDEX,
42
+ status: 'warn',
43
+ message: `归档索引与文件不一致:索引有文件缺 [${missingFiles.slice(0, 5).join(', ')}];文件有索引缺 [${missingIndex.slice(0, 5).join(', ')}](可经 flow archive list / flow archive show 复核)`,
44
+ });
45
+ }
46
+ // b. 活跃集残留(仅非空时产生 warn;判据两段式——REV-80-019):
47
+ // ① 版本前缀 ∈ archived_versions(索引级廉价预筛;归一化派生不单独构成判据——REV-80-001);
48
+ // ② 键 ∈ 对应归档快照的 stages 键集(键级精确证据——「默认版本新建阶段」不误报)。
49
+ // 快照不可读(缺失/损坏,readArchiveFile 读侧宽容 → null)→ 不可证实 → 不报(索引/文件不一致由 a 项承担)。
50
+ const residual = [];
51
+ const snapshotStageKeys = new Map(); // version → 快照 stages 键集(每版本至多读一次)
52
+ for (const stageId of Object.keys(data.stages)) {
53
+ const version = versionOfStageId(stageId);
54
+ if (!version || !indexedSet.has(version)) {
55
+ continue; // 不可解析 或 版本未归档 → 非候选(不读归档文件)
56
+ }
57
+ let keys = snapshotStageKeys.get(version);
58
+ if (keys === undefined) {
59
+ const snapshot = readArchiveFile(projectPath, version);
60
+ keys = new Set(Object.keys(snapshot?.stages ?? {}));
61
+ snapshotStageKeys.set(version, keys);
62
+ }
63
+ if (keys.has(stageId)) {
64
+ residual.push(stageId);
65
+ }
66
+ }
67
+ if (residual.length > 0) {
68
+ items.push({
69
+ section: SECTION_RESIDUAL,
70
+ status: 'warn',
71
+ message: `flow.json 仍含已归档版本阶段:${residual.slice(0, 5).join(', ')}(可能由 checkpoint restore 带回或重复归档残留;须重新执行 flow archive <version> 收敛)`,
72
+ });
73
+ }
74
+ return items;
75
+ }
76
+ //# sourceMappingURL=archive-health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-health.js","sourceRoot":"","sources":["../../../src/core/flow-archive/archive-health.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { FlowData, MetaPhase } from '../flow-manager.js';
2
+ import type { ArchiveFile } from './archive-file.js';
3
+ /** 归档目录:{projectPath}/.openfeel/flow-archive(Q2 定稿:不纳入 WORKSPACE_DIRS,按需 mkdir 幂等创建) */
4
+ export declare function getArchiveDir(projectPath: string): string;
5
+ /**
6
+ * 归档文件路径:{archiveDir}/{version}.json(路径单点)。
7
+ *
8
+ * ⚠️ 仅接受**已校验**的版本字符串(经 `isValidArchiveVersion`,REV-78-011):
9
+ * 本函数只做拼接、不做合法性/路径穿越校验;调用方须先走 `isValidArchiveVersion`
10
+ * 或使用上游已校验入口(`writeArchiveFile` / `readArchiveFile` / `listArchiveVersions`)。
11
+ */
12
+ export declare function getArchiveFilePath(projectPath: string, version: string): string;
13
+ /**
14
+ * 版本字符串合法性(探针法复用 parseStageId,禁自写正则):
15
+ * `${s}-stage-00` 可被 parseStageId 解析且版本段恒等 → 合法;空串/含路径分隔符/非版本形态 → 非法。
16
+ *
17
+ * 导出版(原私有版本校验函数重命名并导出),供命令层复用——REV-79-004。
18
+ */
19
+ export declare function isValidArchiveVersion(s: string): boolean;
20
+ /**
21
+ * 版本号归一(输入前缀兼容):trim → 去单个前导 `v`(如有)→ 补 `v` → 经 `isValidArchiveVersion` 校验。
22
+ * 归一方向恒为「去 v 再加 v」:引擎键恒带 `v`(与 `versionOfStageId` 输出同形态;REV-79-004)。
23
+ * **禁**复用 `normalizeVersion`(`src/core/plan/roadmap.ts:17`,方向相反:去 `v`)。
24
+ *
25
+ * 错误路径返回 null 的触发条件:空串 / 探针校验失败(非 vX.Y[.Z] 形态、含路径分隔符等)。
26
+ */
27
+ export declare function normalizeArchiveVersion(input: string): string | null;
28
+ /**
29
+ * 写归档文件(覆盖写 = 同版本幂等;backup:false,D8 定稿)。
30
+ * 独立锁 `flow-archive` 内原子写(同目录 temp + fsync + rename);目录按需 mkdir(幂等)。
31
+ * @throws 版本字符串非法(路径穿越防御,D9)或写盘失败
32
+ */
33
+ export declare function writeArchiveFile(projectPath: string, file: ArchiveFile): void;
34
+ /**
35
+ * 列举已归档版本(目录扫描 + 文件名合法校验)。
36
+ * 目录不存在 → [];仅收 `{version}.json` 且 version 通过 isValidArchiveVersion 者;返回字典序排序(非语义版本序)。
37
+ */
38
+ export declare function listArchiveVersions(projectPath: string): string[];
39
+ /**
40
+ * 读取归档文件。
41
+ * 缺失 / 损坏(JSON 解析失败)/ 非法版本(路径穿越防御)/ 内容非对象 → null(不抛);
42
+ * 未知键保留(不重建对象);缺 counts → 按实际长度回填(读时计算,不写盘)。
43
+ */
44
+ export declare function readArchiveFile(projectPath: string, version: string): ArchiveFile | null;
45
+ /**
46
+ * 读取已归档版本索引(快照副本,防外部改内存;缺失/非数组 → [])。
47
+ */
48
+ export declare function getArchivedVersions(data: FlowData): string[];
49
+ /**
50
+ * 追加已归档版本(幂等去重;**纯内存**,不写盘——持久化由命令层 save() 承担)。
51
+ * @returns 是否新增(false = 已存在,no-op 不追加)
52
+ */
53
+ export declare function addArchivedVersion(data: FlowData, version: string): boolean;
54
+ /**
55
+ * 归档后 pipeline 处置(**纯内存**,不 IO;save 由调用方统一(裁定 P))。
56
+ *
57
+ * 前置条件:须在 stages 移除**之后**调用(phase 按归档后活跃集重算)。
58
+ * - `pipeline.current.stage` 属被归档版本 → 置空 `{ stage: '', op: '' }`(currentCleared = true)。
59
+ * - `pipeline.phase` 按活跃集重算:非空且全部 done → `'done'`,否则 `'active'`
60
+ * (**空集 → active**,与 `flow-manager.ts:1495-1496` 空集守卫同效;REV-79-005)。
61
+ *
62
+ * @returns currentCleared = current 是否命中被归档版本;phase = 处置后的 pipeline.phase
63
+ */
64
+ export declare function applyArchivePipelineDisposition(data: FlowData, version: string): {
65
+ currentCleared: boolean;
66
+ phase: MetaPhase;
67
+ };
68
+ //# sourceMappingURL=archive-ops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-ops.d.ts","sourceRoot":"","sources":["../../../src/core/flow-archive/archive-ops.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAKrD,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAKxD;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMpE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAW7E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAUjE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAoBxF;AA6BD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,EAAE,CAG5D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAW3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,MAAM,GACd;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAY/C"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * 归档读写原语与索引管理 — 归档引擎的 IO 层
3
+ * 职责:归档文件路径单点 / 写盘(独立锁 flow-archive + 原子写)/ 读取(宽容) / 列举 / meta.archived_versions 管理(纯内存)
4
+ * + pipeline 处置(纯内存)。
5
+ * 写盘与锁复用 src/core/fs/ 工具层(原子写模式 + 建议性文件锁模式);
6
+ * 归档锁名 `flow-archive` 与 flow 锁**不嵌套**(裁定 P;核验 A19)。
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { atomicWriteFileSync } from '../fs/atomic-write.js';
11
+ import { withFileLock, projectLockPath } from '../fs/file-lock.js';
12
+ import { versionOfStageId } from './grouping.js';
13
+ /** 归档写盘锁名(独立于 flow 锁;锁文件落 .openfeel/tmp/locks/flow-archive.lock) */
14
+ const ARCHIVE_LOCK_NAME = 'flow-archive';
15
+ /** 归档目录:{projectPath}/.openfeel/flow-archive(Q2 定稿:不纳入 WORKSPACE_DIRS,按需 mkdir 幂等创建) */
16
+ export function getArchiveDir(projectPath) {
17
+ return join(projectPath, '.openfeel', 'flow-archive');
18
+ }
19
+ /**
20
+ * 归档文件路径:{archiveDir}/{version}.json(路径单点)。
21
+ *
22
+ * ⚠️ 仅接受**已校验**的版本字符串(经 `isValidArchiveVersion`,REV-78-011):
23
+ * 本函数只做拼接、不做合法性/路径穿越校验;调用方须先走 `isValidArchiveVersion`
24
+ * 或使用上游已校验入口(`writeArchiveFile` / `readArchiveFile` / `listArchiveVersions`)。
25
+ */
26
+ export function getArchiveFilePath(projectPath, version) {
27
+ return join(getArchiveDir(projectPath), `${version}.json`);
28
+ }
29
+ /**
30
+ * 版本字符串合法性(探针法复用 parseStageId,禁自写正则):
31
+ * `${s}-stage-00` 可被 parseStageId 解析且版本段恒等 → 合法;空串/含路径分隔符/非版本形态 → 非法。
32
+ *
33
+ * 导出版(原私有版本校验函数重命名并导出),供命令层复用——REV-79-004。
34
+ */
35
+ export function isValidArchiveVersion(s) {
36
+ if (s.length === 0) {
37
+ return false;
38
+ }
39
+ return versionOfStageId(`${s}-stage-00`) === s;
40
+ }
41
+ /**
42
+ * 版本号归一(输入前缀兼容):trim → 去单个前导 `v`(如有)→ 补 `v` → 经 `isValidArchiveVersion` 校验。
43
+ * 归一方向恒为「去 v 再加 v」:引擎键恒带 `v`(与 `versionOfStageId` 输出同形态;REV-79-004)。
44
+ * **禁**复用 `normalizeVersion`(`src/core/plan/roadmap.ts:17`,方向相反:去 `v`)。
45
+ *
46
+ * 错误路径返回 null 的触发条件:空串 / 探针校验失败(非 vX.Y[.Z] 形态、含路径分隔符等)。
47
+ */
48
+ export function normalizeArchiveVersion(input) {
49
+ const trimmed = input.trim();
50
+ // 去单个前导 v(如 `vv1.1.7` 去后剩 `v1.1.7`,仍带 v → 校验失败 → null,防双前缀)
51
+ const body = trimmed.startsWith('v') ? trimmed.slice(1) : trimmed;
52
+ const candidate = `v${body}`;
53
+ return isValidArchiveVersion(candidate) ? candidate : null;
54
+ }
55
+ /**
56
+ * 写归档文件(覆盖写 = 同版本幂等;backup:false,D8 定稿)。
57
+ * 独立锁 `flow-archive` 内原子写(同目录 temp + fsync + rename);目录按需 mkdir(幂等)。
58
+ * @throws 版本字符串非法(路径穿越防御,D9)或写盘失败
59
+ */
60
+ export function writeArchiveFile(projectPath, file) {
61
+ if (!isValidArchiveVersion(file.version)) {
62
+ // 破坏性写路径默认拒绝:非法版本(含路径分隔符/非版本形态)零写盘
63
+ throw new Error(`归档版本非法:${file.version}`);
64
+ }
65
+ const filePath = getArchiveFilePath(projectPath, file.version);
66
+ mkdirSync(getArchiveDir(projectPath), { recursive: true });
67
+ const lockPath = projectLockPath(projectPath, ARCHIVE_LOCK_NAME);
68
+ withFileLock(lockPath, () => {
69
+ atomicWriteFileSync(filePath, JSON.stringify(file, null, 2) + '\n', { backup: false });
70
+ });
71
+ }
72
+ /**
73
+ * 列举已归档版本(目录扫描 + 文件名合法校验)。
74
+ * 目录不存在 → [];仅收 `{version}.json` 且 version 通过 isValidArchiveVersion 者;返回字典序排序(非语义版本序)。
75
+ */
76
+ export function listArchiveVersions(projectPath) {
77
+ const dir = getArchiveDir(projectPath);
78
+ if (!existsSync(dir)) {
79
+ return [];
80
+ }
81
+ return readdirSync(dir)
82
+ .filter((name) => name.endsWith('.json'))
83
+ .map((name) => name.slice(0, -'.json'.length))
84
+ .filter((stem) => isValidArchiveVersion(stem))
85
+ .sort();
86
+ }
87
+ /**
88
+ * 读取归档文件。
89
+ * 缺失 / 损坏(JSON 解析失败)/ 非法版本(路径穿越防御)/ 内容非对象 → null(不抛);
90
+ * 未知键保留(不重建对象);缺 counts → 按实际长度回填(读时计算,不写盘)。
91
+ */
92
+ export function readArchiveFile(projectPath, version) {
93
+ if (!isValidArchiveVersion(version)) {
94
+ return null; // 读侧宽容:非法版本不抛,视同不可读
95
+ }
96
+ const filePath = getArchiveFilePath(projectPath, version);
97
+ if (!existsSync(filePath)) {
98
+ return null;
99
+ }
100
+ try {
101
+ const raw = readFileSync(filePath, 'utf-8');
102
+ const parsed = JSON.parse(raw);
103
+ // 合法 JSON 但非对象(null / 数组 / 数字 / 字符串)→ 视同损坏(契约:缺失/损坏/非法 → null,REV-78-012)
104
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
105
+ return null;
106
+ }
107
+ return withCountsFallback(parsed);
108
+ }
109
+ catch {
110
+ // 损坏(解析失败):返回 null 不崩溃(读侧宽容;kb 读侧分类范式)
111
+ return null;
112
+ }
113
+ }
114
+ /** 读侧宽容:counts 三段齐全则原样返回;否则按实际长度回填(读时计算,不写盘) */
115
+ function withCountsFallback(file) {
116
+ const counts = file.counts;
117
+ if (counts &&
118
+ typeof counts.stages === 'number' &&
119
+ typeof counts.reviews === 'number' &&
120
+ typeof counts.log === 'number') {
121
+ return file;
122
+ }
123
+ const stages = file.stages ?? {};
124
+ const reviews = file.reviews ?? [];
125
+ const log = file.log ?? [];
126
+ return {
127
+ ...file,
128
+ stages,
129
+ reviews,
130
+ log,
131
+ counts: {
132
+ stages: Object.keys(stages).length,
133
+ reviews: reviews.length,
134
+ log: log.length,
135
+ },
136
+ };
137
+ }
138
+ /**
139
+ * 读取已归档版本索引(快照副本,防外部改内存;缺失/非数组 → [])。
140
+ */
141
+ export function getArchivedVersions(data) {
142
+ const list = data.meta?.archived_versions;
143
+ return Array.isArray(list) ? [...list] : [];
144
+ }
145
+ /**
146
+ * 追加已归档版本(幂等去重;**纯内存**,不写盘——持久化由命令层 save() 承担)。
147
+ * @returns 是否新增(false = 已存在,no-op 不追加)
148
+ */
149
+ export function addArchivedVersion(data, version) {
150
+ if (!data.meta) {
151
+ // 防御:缺 meta 的存量/损坏数据(与 save() 的 ??= 兜底同字面量)
152
+ data.meta = { version: '1.0', project: '', updated: '', revision: 0 };
153
+ }
154
+ const existing = getArchivedVersions(data);
155
+ if (existing.includes(version)) {
156
+ return false; // 幂等:重复登记不追加
157
+ }
158
+ data.meta.archived_versions = [...existing, version];
159
+ return true;
160
+ }
161
+ /**
162
+ * 归档后 pipeline 处置(**纯内存**,不 IO;save 由调用方统一(裁定 P))。
163
+ *
164
+ * 前置条件:须在 stages 移除**之后**调用(phase 按归档后活跃集重算)。
165
+ * - `pipeline.current.stage` 属被归档版本 → 置空 `{ stage: '', op: '' }`(currentCleared = true)。
166
+ * - `pipeline.phase` 按活跃集重算:非空且全部 done → `'done'`,否则 `'active'`
167
+ * (**空集 → active**,与 `flow-manager.ts:1495-1496` 空集守卫同效;REV-79-005)。
168
+ *
169
+ * @returns currentCleared = current 是否命中被归档版本;phase = 处置后的 pipeline.phase
170
+ */
171
+ export function applyArchivePipelineDisposition(data, version) {
172
+ const current = data.pipeline?.current;
173
+ // 命中被归档版本 → 置空 current(stage 与 op 同清;versionOfStageId('') === null 故空串永不命中)
174
+ const currentCleared = !!current && versionOfStageId(current.stage) === version;
175
+ if (currentCleared && data.pipeline) {
176
+ data.pipeline.current = { stage: '', op: '' };
177
+ }
178
+ // 宏观 done 判定(空集守卫:无阶段视为非全 done → active)
179
+ const allDone = Object.keys(data.stages).length > 0
180
+ && Object.values(data.stages).every((s) => s.phase === 'done');
181
+ data.pipeline.phase = allDone ? 'done' : 'active';
182
+ return { currentCleared, phase: data.pipeline.phase };
183
+ }
184
+ //# sourceMappingURL=archive-ops.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-ops.js","sourceRoot":"","sources":["../../../src/core/flow-archive/archive-ops.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAIjD,oEAAoE;AACpE,MAAM,iBAAiB,GAAG,cAAc,CAAC;AAEzC,wFAAwF;AACxF,MAAM,UAAU,aAAa,CAAC,WAAmB;IAC/C,OAAO,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,OAAe;IACrE,OAAO,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,GAAG,OAAO,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,CAAS;IAC7C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,gBAAgB,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,4DAA4D;IAC5D,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAClE,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC;IAC7B,OAAO,qBAAqB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,IAAiB;IACrE,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACzC,mCAAmC;QACnC,MAAM,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/D,SAAS,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3D,MAAM,QAAQ,GAAG,eAAe,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;IACjE,YAAY,CAAC,QAAQ,EAAE,GAAG,EAAE;QAC1B,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAmB;IACrD,MAAM,GAAG,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;IACvC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,WAAW,CAAC,GAAG,CAAC;SACpB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;SACxC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;SAC7C,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;SAC7C,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,OAAe;IAClE,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC,CAAC,oBAAoB;IACnC,CAAC;IACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC1D,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,0EAA0E;QAC1E,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,kBAAkB,CAAC,MAAqB,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,uCAAuC;QACvC,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,gDAAgD;AAChD,SAAS,kBAAkB,CAAC,IAAiB;IAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,IACE,MAAM;QACN,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;QACjC,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ;QAClC,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,EAC9B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;IAC3B,OAAO;QACL,GAAG,IAAI;QACP,MAAM;QACN,OAAO;QACP,GAAG;QACH,MAAM,EAAE;YACN,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM;YAClC,OAAO,EAAE,OAAO,CAAC,MAAM;YACvB,GAAG,EAAE,GAAG,CAAC,MAAM;SAChB;KACF,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAc;IAChD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAc,EAAE,OAAe;IAChE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,4CAA4C;QAC5C,IAAI,CAAC,IAAI,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACxE,CAAC;IACD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAC,CAAC,aAAa;IAC7B,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,iBAAiB,GAAG,CAAC,GAAG,QAAQ,EAAE,OAAO,CAAC,CAAC;IACrD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,+BAA+B,CAC7C,IAAc,EACd,OAAe;IAEf,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC;IACvC,4EAA4E;IAC5E,MAAM,cAAc,GAAG,CAAC,CAAC,OAAO,IAAI,gBAAgB,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,OAAO,CAAC;IAChF,IAAI,cAAc,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACpC,IAAI,CAAC,QAAQ,CAAC,OAAO,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;IAChD,CAAC;IACD,yCAAyC;IACzC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC;WAC9C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC;IACjE,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;IAClD,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;AACxD,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { FlowData, StageData, ReviewItem, LogEntry } from '../flow-manager.js';
2
+ import type { ArchiveFile } from './archive-file.js';
3
+ /** splitForArchive 返回:归档段(缺 schemaVersion/archived_at,由命令层装配) + 保留段 */
4
+ export interface SplitResult {
5
+ /** 归档段:不含 schemaVersion / archived_at(由 stage-79 命令层装配后交 writeArchiveFile) */
6
+ archive: Omit<ArchiveFile, 'schemaVersion' | 'archived_at'>;
7
+ /** 保留段:留在 flow.json 的条目(不可归档归属者一律落此) */
8
+ remainder: {
9
+ stages: Record<string, StageData>;
10
+ reviews: ReviewItem[];
11
+ log: LogEntry[];
12
+ };
13
+ }
14
+ /** 可归档候选(供命令面判据门消费;blocking REV 判定归命令层,本函数只读纯判定) */
15
+ export interface ArchivableCandidate {
16
+ /** 版本前缀(如 `v1.1.7`) */
17
+ version: string;
18
+ /** 该版本全部 stageId(保持 stages 键遍历顺序) */
19
+ stages: string[];
20
+ /** 该版本是否全部阶段 phase=done(空集守卫:length>0 && every(done)) */
21
+ allDone: boolean;
22
+ }
23
+ /**
24
+ * stageId → 版本前缀(如 `v1.1.7`)。
25
+ * 复用 parseStageId(唯一权威,禁自写正则):fullStageId = `${version}-${stageDir}`,剥离 `-${stageDir}` 后缀即版本段。
26
+ * 短名 stageId 经 parseStageId 归一化为 `v1.0.0-*`(DEFAULT_STAGE_VERSION,既定语义)。
27
+ * 无法解析返回 null。
28
+ */
29
+ export declare function versionOfStageId(stageId: string): string | null;
30
+ /**
31
+ * stages 按版本前缀分组(版本 → stageId 列表)。
32
+ * 保持稳定顺序(按 stages 键遍历顺序);无法解析的 stageId 跳过(不产生分组 → 永不归档)。
33
+ */
34
+ export declare function groupStagesByVersion(stages: Record<string, StageData>): Map<string, string[]>;
35
+ /**
36
+ * 可归档候选列表(只读纯函数):版本 + 该版本 stageId 列表 + 是否全部 phase=done。
37
+ * allDone 采用空集守卫(length > 0 && every(done),对齐 flow-manager.ts:1484-1486 范式——空集不判 done)。
38
+ * 不判 REV(blocking open REV 判定归命令层,裁定 O2)。
39
+ */
40
+ export declare function listArchivableCandidates(data: FlowData): ArchivableCandidate[];
41
+ /**
42
+ * 版本作用域内的「阻塞 open」审查条目(只读纯函数;返回新数组副本,push 不影响 data.reviews)。
43
+ *
44
+ * 谓词为**全仓唯一**口径(对齐 `flow-manager.ts:1456`/`:1570`;REV-79-002):
45
+ * `r.blocking !== false && r.status === 'open'` —— blocking 缺省(undefined)= 阻塞。
46
+ * 版本作用域解析与 splitForArchive **同源**(复用本文件私有 versionOfReviewOp)。
47
+ */
48
+ export declare function listBlockingOpenRevsForVersion(data: FlowData, version: string): ReviewItem[];
49
+ /**
50
+ * 按版本切分 flow 数据(纯函数,不写盘)。
51
+ * - stages:该版本全部 stageId 条目整体搬入 archive;其余(含不可解析 stageId)保留 remainder。
52
+ * - reviews:`versionOfReviewOp(r.op) === version` 者搬入;其余保留(保序)。
53
+ * - log:`versionOfLogEntry(e) === version` 者搬入;其余保留(保序;无 stageName / 不可解析一律保留)。
54
+ * - 版本不存在 → archive 为空集(counts 全 0),不抛错。
55
+ * 返回集合中的对象为原数据浅引用(调用方不得修改)。
56
+ */
57
+ export declare function splitForArchive(data: FlowData, version: string): SplitResult;
58
+ //# sourceMappingURL=grouping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grouping.d.ts","sourceRoot":"","sources":["../../../src/core/flow-archive/grouping.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC;IAC5D,wCAAwC;IACxC,SAAS,EAAE;QACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAClC,OAAO,EAAE,UAAU,EAAE,CAAC;QACtB,GAAG,EAAE,QAAQ,EAAE,CAAC;KACjB,CAAC;CACH;AAED,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,yDAAyD;IACzD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU/D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAe7F;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,GAAG,mBAAmB,EAAE,CAO9E;AAiBD;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAI5F;AAeD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAiD5E"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * 版本分组与切分纯函数 — 归档引擎的解析核心
3
+ * 职责:stageId → 版本前缀(复用 parseStageId,禁自写正则);
4
+ * 按版本分组;按版本切分 flow 数据(stages 全搬 / reviews 按 op 前缀 / log 按 detail.stageName)。
5
+ * 本文件全部为纯函数(只读输入,不写盘、不加锁、不读环境)。
6
+ */
7
+ import { parseStageId } from '../plan/path.js';
8
+ /**
9
+ * stageId → 版本前缀(如 `v1.1.7`)。
10
+ * 复用 parseStageId(唯一权威,禁自写正则):fullStageId = `${version}-${stageDir}`,剥离 `-${stageDir}` 后缀即版本段。
11
+ * 短名 stageId 经 parseStageId 归一化为 `v1.0.0-*`(DEFAULT_STAGE_VERSION,既定语义)。
12
+ * 无法解析返回 null。
13
+ */
14
+ export function versionOfStageId(stageId) {
15
+ const parsed = parseStageId(stageId);
16
+ if (!parsed) {
17
+ return null;
18
+ }
19
+ const suffix = `-${parsed.stageDir}`;
20
+ if (!parsed.fullStageId.endsWith(suffix)) {
21
+ return null;
22
+ }
23
+ return parsed.fullStageId.slice(0, -suffix.length);
24
+ }
25
+ /**
26
+ * stages 按版本前缀分组(版本 → stageId 列表)。
27
+ * 保持稳定顺序(按 stages 键遍历顺序);无法解析的 stageId 跳过(不产生分组 → 永不归档)。
28
+ */
29
+ export function groupStagesByVersion(stages) {
30
+ const groups = new Map();
31
+ for (const stageId of Object.keys(stages)) {
32
+ const version = versionOfStageId(stageId);
33
+ if (!version) {
34
+ continue; // 无法解析 → 不分组(保留在 flow.json)
35
+ }
36
+ const list = groups.get(version);
37
+ if (list) {
38
+ list.push(stageId);
39
+ }
40
+ else {
41
+ groups.set(version, [stageId]);
42
+ }
43
+ }
44
+ return groups;
45
+ }
46
+ /**
47
+ * 可归档候选列表(只读纯函数):版本 + 该版本 stageId 列表 + 是否全部 phase=done。
48
+ * allDone 采用空集守卫(length > 0 && every(done),对齐 flow-manager.ts:1484-1486 范式——空集不判 done)。
49
+ * 不判 REV(blocking open REV 判定归命令层,裁定 O2)。
50
+ */
51
+ export function listArchivableCandidates(data) {
52
+ const result = [];
53
+ for (const [version, stageIds] of groupStagesByVersion(data.stages)) {
54
+ const allDone = stageIds.length > 0 && stageIds.every((id) => data.stages[id]?.phase === 'done');
55
+ result.push({ version, stages: stageIds, allDone });
56
+ }
57
+ return result;
58
+ }
59
+ /**
60
+ * reviews 条目 → 版本前缀。
61
+ * `op` 形如 `<stageId>.op-NNN` 或 `<stageId>`:点号锚定正则 `/^(.+)\.(op-\d+)$/` 剥离 opId 段
62
+ * (stageId 自身可含点号,禁 split('.');见 archive/merge.ts:57-67 既有范式);stageId 段经 versionOfStageId。
63
+ * 无法归属返回 null(→ 保留在 flow.json)。
64
+ */
65
+ function versionOfReviewOp(op) {
66
+ if (typeof op !== 'string' || op.length === 0) {
67
+ return null;
68
+ }
69
+ const anchor = op.match(/^(.+)\.(op-\d+)$/);
70
+ const stageId = anchor ? anchor[1] : op;
71
+ return versionOfStageId(stageId);
72
+ }
73
+ /**
74
+ * 版本作用域内的「阻塞 open」审查条目(只读纯函数;返回新数组副本,push 不影响 data.reviews)。
75
+ *
76
+ * 谓词为**全仓唯一**口径(对齐 `flow-manager.ts:1456`/`:1570`;REV-79-002):
77
+ * `r.blocking !== false && r.status === 'open'` —— blocking 缺省(undefined)= 阻塞。
78
+ * 版本作用域解析与 splitForArchive **同源**(复用本文件私有 versionOfReviewOp)。
79
+ */
80
+ export function listBlockingOpenRevsForVersion(data, version) {
81
+ return (data.reviews ?? []).filter((r) => versionOfReviewOp(r.op) === version && r.blocking !== false && r.status === 'open');
82
+ }
83
+ /**
84
+ * log 条目 → 版本前缀。
85
+ * 唯一读 `detail.stageName`(裁定 T);缺失 / 非字符串 / 不可解析 → null(保留在 flow.json)。
86
+ * 注意:`detail.stageId` / `detail.stage` 等其它键一律不看(保守裁定,见 D3)。
87
+ */
88
+ function versionOfLogEntry(entry) {
89
+ const stageName = entry?.detail?.['stageName'];
90
+ if (typeof stageName !== 'string') {
91
+ return null;
92
+ }
93
+ return versionOfStageId(stageName);
94
+ }
95
+ /**
96
+ * 按版本切分 flow 数据(纯函数,不写盘)。
97
+ * - stages:该版本全部 stageId 条目整体搬入 archive;其余(含不可解析 stageId)保留 remainder。
98
+ * - reviews:`versionOfReviewOp(r.op) === version` 者搬入;其余保留(保序)。
99
+ * - log:`versionOfLogEntry(e) === version` 者搬入;其余保留(保序;无 stageName / 不可解析一律保留)。
100
+ * - 版本不存在 → archive 为空集(counts 全 0),不抛错。
101
+ * 返回集合中的对象为原数据浅引用(调用方不得修改)。
102
+ */
103
+ export function splitForArchive(data, version) {
104
+ const archiveStages = {};
105
+ const remainderStages = {};
106
+ for (const [stageId, stage] of Object.entries(data.stages)) {
107
+ if (versionOfStageId(stageId) === version) {
108
+ archiveStages[stageId] = stage;
109
+ }
110
+ else {
111
+ remainderStages[stageId] = stage;
112
+ }
113
+ }
114
+ const archiveReviews = [];
115
+ const remainderReviews = [];
116
+ for (const review of data.reviews ?? []) {
117
+ if (versionOfReviewOp(review.op) === version) {
118
+ archiveReviews.push(review);
119
+ }
120
+ else {
121
+ remainderReviews.push(review);
122
+ }
123
+ }
124
+ const archiveLog = [];
125
+ const remainderLog = [];
126
+ for (const entry of data.log ?? []) {
127
+ if (versionOfLogEntry(entry) === version) {
128
+ archiveLog.push(entry);
129
+ }
130
+ else {
131
+ remainderLog.push(entry);
132
+ }
133
+ }
134
+ return {
135
+ archive: {
136
+ version,
137
+ stages: archiveStages,
138
+ reviews: archiveReviews,
139
+ log: archiveLog,
140
+ counts: {
141
+ stages: Object.keys(archiveStages).length,
142
+ reviews: archiveReviews.length,
143
+ log: archiveLog.length,
144
+ },
145
+ },
146
+ remainder: {
147
+ stages: remainderStages,
148
+ reviews: remainderReviews,
149
+ log: remainderLog,
150
+ },
151
+ };
152
+ }
153
+ //# sourceMappingURL=grouping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grouping.js","sourceRoot":"","sources":["../../../src/core/flow-archive/grouping.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA0B/C;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;IACrC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACrD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAiC;IACpE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1C,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,SAAS,CAAC,4BAA4B;QACxC,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACjC,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAc;IACrD,MAAM,MAAM,GAA0B,EAAE,CAAC;IACzC,KAAK,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACpE,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,KAAK,KAAK,MAAM,CAAC,CAAC;QACjG,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,EAAU;IACnC,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACxC,OAAO,gBAAgB,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,8BAA8B,CAAC,IAAc,EAAE,OAAe;IAC5E,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAChC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,CAC1F,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,KAAe;IACxC,MAAM,SAAS,GAAG,KAAK,EAAE,MAAM,EAAE,CAAC,WAAW,CAAC,CAAC;IAC/C,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,IAAc,EAAE,OAAe;IAC7D,MAAM,aAAa,GAA8B,EAAE,CAAC;IACpD,MAAM,eAAe,GAA8B,EAAE,CAAC;IACtD,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3D,IAAI,gBAAgB,CAAC,OAAO,CAAC,KAAK,OAAO,EAAE,CAAC;YAC1C,aAAa,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;QACjC,CAAC;aAAM,CAAC;YACN,eAAe,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;QACnC,CAAC;IACH,CAAC;IAED,MAAM,cAAc,GAAiB,EAAE,CAAC;IACxC,MAAM,gBAAgB,GAAiB,EAAE,CAAC;IAC1C,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACxC,IAAI,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,OAAO,EAAE,CAAC;YAC7C,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAe,EAAE,CAAC;IAClC,MAAM,YAAY,GAAe,EAAE,CAAC;IACpC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC;QACnC,IAAI,iBAAiB,CAAC,KAAK,CAAC,KAAK,OAAO,EAAE,CAAC;YACzC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE;YACP,OAAO;YACP,MAAM,EAAE,aAAa;YACrB,OAAO,EAAE,cAAc;YACvB,GAAG,EAAE,UAAU;YACf,MAAM,EAAE;gBACN,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM;gBACzC,OAAO,EAAE,cAAc,CAAC,MAAM;gBAC9B,GAAG,EAAE,UAAU,CAAC,MAAM;aACvB;SACF;QACD,SAAS,EAAE;YACT,MAAM,EAAE,eAAe;YACvB,OAAO,EAAE,gBAAgB;YACzB,GAAG,EAAE,YAAY;SAClB;KACF,CAAC;AACJ,CAAC"}