ai-delivery-workflow 0.5.1 → 0.7.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 (53) hide show
  1. package/docs/CLI-PARAMETER-REFERENCE.zh-CN.md +1 -0
  2. package/docs/FILE-REFERENCE.zh-CN.md +3 -3
  3. package/docs/MAINTENANCE-VERIFICATION.zh-CN.md +2 -2
  4. package/docs/PROJECT-MANUAL.zh-CN.md +10 -1
  5. package/docs/agents/workflow-manager-current-state.md +1 -1
  6. package/lib/communication-confirmation.mjs +177 -0
  7. package/lib/project-bootstrap.mjs +350 -3
  8. package/lib/project-installer.mjs +25 -1
  9. package/package.json +1 -1
  10. package/skills/ai-delivery-assemble-release/SKILL.md +4 -0
  11. package/skills/ai-delivery-bootstrap/SKILL.md +6 -0
  12. package/skills/ai-delivery-checkpoint-task/SKILL.md +4 -0
  13. package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +2 -0
  14. package/skills/ai-delivery-close-version/SKILL.md +15 -10
  15. package/skills/ai-delivery-close-version/references/version-closeout-contract.md +2 -1
  16. package/skills/ai-delivery-define-product/SKILL.md +8 -3
  17. package/skills/ai-delivery-deploy-production/SKILL.md +2 -0
  18. package/skills/ai-delivery-design-architecture/SKILL.md +12 -5
  19. package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-gate.yaml +2 -0
  20. package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +2 -0
  21. package/skills/ai-delivery-design-architecture/references/architecture-contract.md +4 -0
  22. package/skills/ai-delivery-design-experience/SKILL.md +7 -3
  23. package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +2 -0
  24. package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +3 -0
  25. package/skills/ai-delivery-design-experience/references/experience-contract.md +2 -2
  26. package/skills/ai-delivery-design-tests/SKILL.md +6 -0
  27. package/skills/ai-delivery-develop-iteration/SKILL.md +8 -1
  28. package/skills/ai-delivery-develop-iteration/assets/development-template/candidate-manifest.yaml +2 -0
  29. package/skills/ai-delivery-evolve-workflow/SKILL.md +2 -0
  30. package/skills/ai-delivery-execute-work-package/SKILL.md +6 -2
  31. package/skills/ai-delivery-manage-standards/SKILL.md +2 -0
  32. package/skills/ai-delivery-orchestrate/SKILL.md +4 -0
  33. package/skills/ai-delivery-orchestrate/assets/codebase-template/AGENTS.md +14 -0
  34. package/skills/ai-delivery-orchestrate/assets/codebase-template/CODEBASE_GUIDE.md +124 -0
  35. package/skills/ai-delivery-orchestrate/assets/codebase-template/docs/CODEBASE_INDEX.md +87 -0
  36. package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-examples.yaml +67 -0
  37. package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-summary.yaml +42 -0
  38. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/iterations/ITER-0001/02-solution-design/product-impact-assessment.yaml +42 -0
  39. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/bootstrap/bootstrap.mjs +7 -2
  40. package/skills/ai-delivery-orchestrate/references/codebase-document-contract.md +93 -0
  41. package/skills/ai-delivery-orchestrate/references/communication-confirmation-contract.md +69 -0
  42. package/skills/ai-delivery-orchestrate/references/product-impact-assessment-contract.md +87 -0
  43. package/skills/ai-delivery-orchestrate/scripts/confirmation-state.mjs +30 -0
  44. package/skills/ai-delivery-orchestrate-release/SKILL.md +5 -1
  45. package/skills/ai-delivery-plan-iteration/SKILL.md +8 -1
  46. package/skills/ai-delivery-prepare-platform/SKILL.md +3 -1
  47. package/skills/ai-delivery-prepare-release/SKILL.md +4 -0
  48. package/skills/ai-delivery-review-change/SKILL.md +5 -1
  49. package/skills/ai-delivery-validate-artifacts/SKILL.md +12 -7
  50. package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +2 -1
  51. package/skills/ai-delivery-verify-candidate/SKILL.md +7 -2
  52. package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +1 -1
  53. package/skills/ai-delivery-verify-production/SKILL.md +4 -0
@@ -58,6 +58,7 @@ npx ai-delivery-workflow@0.4.0 doctor .
58
58
  | `repair` | 追加独立的前向归档失效事务;原归档证据保持逐字节不变。 | 视参数而定 |
59
59
  | `inspect` | 只读识别工作区、两套 Git、安装完整性、正式状态和任务恢复建议。 | 否 |
60
60
  | `bootstrap` | 根据只读检查决定初始化缺失工作流、恢复 Agent 责任任务或停在用户/外部输入边界。 | 视路由而定 |
61
+ | `node .workflow/tools/bootstrap/bootstrap.mjs project-index` | 从代码仓库 `docs/CODEBASE_INDEX.md` 生成工作流仓库内的 `codebase-index.yaml` 派生投影,并记录源文件 checksum。 | 是;输出路径必须在代码仓库之外 |
61
62
  | `doctor` | 校验受管文件 checksum、Skill、Hook、CLI、Workflow Manager、必需目录和持久项目配置;排除本地 runtime。 | 否 |
62
63
  | `codegraph setup` | 对权威代码仓关闭 CodeGraph telemetry、按当前 `CODEGRAPH_DIR` 登记本地 ignore 并显式初始化索引;不安装 CLI 或全局 MCP。 | 是;`--dry-run` 时否 |
63
64
  | `codegraph sync` | 增量同步已有 CodeGraph 索引并重新验证索引目标。 | 是;`--dry-run` 时否 |
@@ -3,7 +3,7 @@
3
3
  > 当前双仓目录、初始化规则、状态分层和 Git 边界见 [DUAL-REPOSITORY-WORKSPACE.zh-CN.md](DUAL-REPOSITORY-WORKSPACE.zh-CN.md)。
4
4
 
5
5
  > 适用目录:工作流发行仓库
6
- > 适用包版本:`ai-delivery-workflow@0.5.1`
6
+ > 适用包版本:`ai-delivery-workflow@0.6.0`
7
7
  > 使用方法和完整流程:[PROJECT-MANUAL.zh-CN.md](PROJECT-MANUAL.zh-CN.md)
8
8
  > CLI 安装参数:[CLI-PARAMETER-REFERENCE.zh-CN.md](CLI-PARAMETER-REFERENCE.zh-CN.md)
9
9
  > CodeGraph 可选集成:[CODEGRAPH-INTEGRATION.zh-CN.md](CODEGRAPH-INTEGRATION.zh-CN.md)
@@ -79,7 +79,7 @@
79
79
  | `lib/mock-system.mjs` | Microcks 项目适配器;固定镜像版本与 digest,管理并在健康失败时清理 loopback 易失容器,隔离 `external-dependencies.yaml` 与 `product-backends.yaml`,校验 OpenAPI/AsyncAPI operation、channel、具名 scenario 与 dispatcher 输入,并通过 multipart 本地 API 同步合同。 |
80
80
  | `lib/mock-scenario-pack.mjs` | Mock Scenario Pack 冻结器;绑定 Prototype revision、全部 Review Scene 唯一映射、受管 UI 实际导出物及其协议/场景 inventory、合同和脱敏证据 checksum,执行人工 candidate checksum 确认并拒绝覆盖不可变 Pack。 |
81
81
  | `lib/project-repair.mjs` | 归档完整性前向修复器;校验人工 actor、原因和证据,追加 checksum 链式失效事务并保留原归档字节。 |
82
- | `lib/project-bootstrap.mjs` | 只读扫描仓库并分类五种项目状态;识别代码、技术、Git、正式工作流和 runtime;为新接入项目生成草稿基线、文件清单、上下文索引、接入评估和引导计划;中断项目直接路由 checkpoint。 |
82
+ | `lib/project-bootstrap.mjs` | 只读扫描仓库并分类五种项目状态;识别代码、技术、Git、正式工作流和 runtime;为新接入项目生成草稿基线、文件清单、上下文索引、接入评估和引导计划;从代码仓库 `docs/CODEBASE_INDEX.md` 生成带 checksum 的工作流仓库机器投影;中断项目直接路由 checkpoint。 |
83
83
  | `lib/yaml-runtime.mjs` | 为发行包和安装后的项目内 Bootstrap 运行时解析 YAML;优先使用包依赖,项目内回退到状态 CLI 的 vendored YAML。 |
84
84
  | `lib/delivery-state.mjs` | 正式状态公共命令实现;管理物料、节点、Gate、候选、发布和可信历史锚点,提供 revision、锁、原子写、checksum、例行/增量/全历史校验。 |
85
85
  | `lib/evolution.mjs` | 项目级自进化公共命令实现;管理反馈、提案、风险、扩展版本、批准、trial、Hook 调度、回退和 promotion bundle。 |
@@ -189,7 +189,7 @@
189
189
  | `skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs` | 完整项目模板中的 Hook 记录器副本;与 checkpoint skill 的脚本保持一致,供整体模板审阅。 |
190
190
  | `skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/state/state.mjs` | 安装后正式状态 CLI wrapper;使用 vendored YAML 调用统一状态模块。 |
191
191
  | `skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/evolution/evolve.mjs` | 安装后 Evolution CLI wrapper;复用状态运行时 vendored YAML 并调用统一 Evolution 模块。 |
192
- | `skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/bootstrap/bootstrap.mjs` | 安装后自主入口 wrapper;提供不依赖全局命令的只读识别和引导上下文生成。 |
192
+ | `skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/bootstrap/bootstrap.mjs` | 安装后自主入口 wrapper;提供不依赖全局命令的只读识别、引导上下文生成和 `project-index` 机器投影命令。 |
193
193
  | `skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/index.yaml` | 初始自进化索引,schema 1;不伪造反馈、提案或扩展。 |
194
194
  | `skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/manager.mjs` | 安装后 Workflow Manager wrapper;从受管 runtime 调用同一应用服务。 |
195
195
  | `skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/start.cmd` | Windows CMD Manager 启动入口。 |
@@ -5,8 +5,8 @@
5
5
  ## 1. npm 发布预检
6
6
 
7
7
  ```powershell
8
- npm run release:check -- prepare --version 0.5.1
9
- npm run release:check -- verify --version 0.5.1 --commit <40位提交ID>
8
+ npm run release:check -- prepare --version 0.6.0
9
+ npm run release:check -- verify --version 0.6.0 --commit <40位提交ID>
10
10
  ```
11
11
 
12
12
  `prepare` 默认执行 `full` 检查:清单与锁文件版本、`main`、干净工作树、Tag 可用性、全量测试、25 个 Skill、审计验证、`npm pack --dry-run`、`npm publish --dry-run`、官方 registry、版本未占用、npm 身份和 `auth-and-writes` 2FA。两个 dry-run 还必须返回一致的包名、版本、文件清单、文件数量和制品摘要;本包必须包含 `bin/`、`docs/`、`lib/`、`skills/` 及有效的 `bin.ai-delivery`。任一项失败都会输出 `decision: fail` 并返回非零退出码。
@@ -18,7 +18,7 @@ workspace/code/.git 代码 Git
18
18
  初始化器还会合并根 `.gitattributes`,对 `.workflow/**`、项目级 `ai-delivery-*` Skills、`.codex/config.toml`、`AGENTS.md`、`.gitignore` 和 `.gitattributes` 强制 `eol=lf`。这是 checksum 可移植性约束;不会修改嵌套代码仓的换行策略。已冻结的旧迭代证据必须在根 `.gitattributes` 中使用精确路径,或“固定迭代目录 + 文件名 glob”的窄 `-text` 规则登记后,才能保留 raw checksum;嵌套或全局 attributes 不能自行获得该例外。产品、控制、工具等其他路径和 `text eol=crlf` 覆盖也不享受该例外。缺失或无效规则会使 `doctor` 失败,避免 Windows checkout 后正式状态和物料被误判为篡改。
19
19
 
20
20
  > 文档版本:1.0
21
- > 适用包版本:`ai-delivery-workflow@0.5.1`
21
+ > 适用包版本:`ai-delivery-workflow@0.6.0`
22
22
  > 默认文档语言:简体中文
23
23
  > 文件级索引:[FILE-REFERENCE.zh-CN.md](FILE-REFERENCE.zh-CN.md)
24
24
  > CodeGraph 可选集成:[CODEGRAPH-INTEGRATION.zh-CN.md](CODEGRAPH-INTEGRATION.zh-CN.md)
@@ -262,6 +262,7 @@ npx ai-delivery-workflow@latest upgrade .
262
262
  | `ai-delivery inspect [project]` | 否 | 输出机器可读的项目、代码、流程和运行时状态。 |
263
263
  | `ai-delivery bootstrap [project]` | 视状态而定 | 初始化空项目或存量项目上下文,或把中断项目路由到恢复。 |
264
264
  | `ai-delivery bootstrap [project] --dry-run` | 否 | 预览引导动作。 |
265
+ | `node .workflow/tools/bootstrap/bootstrap.mjs project-index [project] [--output <path>]` | 是(写入工作流仓库) | 读取代码仓库唯一事实源 `docs/CODEBASE_INDEX.md`,生成带 `source_checksum` 的 `.workflow/delivery/bootstrap/codebase-index.yaml` 机器投影;投影不得写回代码仓库。 |
265
266
  | `ai-delivery doctor [project]` | 否 | 校验发行包表面、安装清单、关键 skills、Hook、运行时和持久项目文件;排除 `.workflow/delivery/runtime`,状态 inspect 使用 16 MiB 有界缓冲并验证 JSON 协议,Git attributes 使用有界批次检查。 |
266
267
  | `ai-delivery codegraph setup [project]` | 是 | 显式关闭 telemetry,并为配置中的代码仓初始化本地 CodeGraph 索引。 |
267
268
  | `ai-delivery codegraph setup [project] --dry-run` | 否 | 只读验证 `CODEGRAPH_DIR`、索引文件与 Git 边界,只调用 `codegraph --version`,并输出 telemetry、local exclude 和初始化计划。 |
@@ -308,6 +309,14 @@ Page 决定和修改要求绑定精确 Revision、Page/Scene 及 checksum。修
308
309
  node .workflow/tools/bootstrap/bootstrap.mjs inspect
309
310
  ```
310
311
 
312
+ 代码仓库说明索引由维护者编辑 `docs/CODEBASE_INDEX.md`。需要给 Agent 或工具快速读取时,在工作流仓库根目录执行:
313
+
314
+ ```powershell
315
+ node .workflow/tools/bootstrap/bootstrap.mjs project-index <project>
316
+ ```
317
+
318
+ 该命令只生成派生 YAML,消费者必须校验 `source_checksum`;不要直接编辑投影或把投影复制回代码仓库。
319
+
311
320
  引导器只返回以下五种状态之一:
312
321
 
313
322
  | 状态 | 含义 | 推荐动作 |
@@ -7,7 +7,7 @@
7
7
  ## 1. 当前基线
8
8
 
9
9
  - 当前仓库是 `ai-delivery-workflow` 的维护与发行源码仓库,不是安装了工作流的业务项目。
10
- - 当前正式源码候选版本为 `0.5.1`(待完成发布 Gate);上一版 `0.5.0` 的提交、Git tag、npm 包身份及 Workflow Manager 的历史 `0.4.0` 候选身份、校验和和生命周期证据仍记录在维护仓库的 `audit/WORKFLOW-MANAGER-P0-P1-P2-CLOSURE.md`,这些历史记录不可改写。候选包和展开验证工作区保留在被忽略的 `.tmp/`。
10
+ - 当前正式源码候选版本为 `0.6.0`(待完成发布 Gate);上一版 `0.5.1` 的提交、Git tag、npm 包身份及 Workflow Manager 的历史 `0.4.0` 候选身份、校验和和生命周期证据仍记录在维护仓库的 `audit/WORKFLOW-MANAGER-P0-P1-P2-CLOSURE.md`,这些历史记录不可改写。候选包和展开验证工作区保留在被忽略的 `.tmp/`。
11
11
  - 正式维护源码位于 `bin/`、`lib/`、`skills/`、`docs/`、`verification/` 和 `audit/`。
12
12
  - Workflow Manager 设计原型位于 `prototypes/workflow-manager/`,业务项目原型夹具位于 `prototypes/order-ops-business/`。两者是可版本化设计资产,不进入发行包,也不等于正式前端已经实现;运行时依赖、构建产物和一次性验证工作区仍放在 `.tmp/`。
13
13
 
@@ -0,0 +1,177 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { parse as parseYaml, stringify as stringifyYaml } from "./yaml-runtime.mjs";
5
+
6
+ export const CONFIRMATION_STATUSES = new Set(["proposed", "confirmed", "rejected", "deferred", "superseded"]);
7
+ export const CONFIRMATION_CATEGORIES = new Set([
8
+ "observed_fact", "business_fact", "business_decision", "domain_term", "business_constraint",
9
+ "architecture_decision", "standard_decision", "risk", "assumption", "open_question",
10
+ "execution_fact", "execution_decision",
11
+ ]);
12
+ export const NEXT_ACTION_OWNERS = new Set(["agent", "user", "external"]);
13
+
14
+ const CLOSED_STATUSES = new Set(["confirmed", "rejected", "superseded"]);
15
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
16
+
17
+ function text(value) {
18
+ return typeof value === "string" && value.trim().length > 0;
19
+ }
20
+
21
+ function list(value) {
22
+ return Array.isArray(value) ? value : [];
23
+ }
24
+
25
+ function addError(errors, location, message) {
26
+ errors.push(`${location}: ${message}`);
27
+ }
28
+
29
+ function stableValue(value) {
30
+ if (Array.isArray(value)) return value.map(stableValue);
31
+ if (value && typeof value === "object") {
32
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
33
+ }
34
+ return value;
35
+ }
36
+
37
+ function sameRecord(left, right) {
38
+ return JSON.stringify(stableValue(left)) === JSON.stringify(stableValue(right));
39
+ }
40
+
41
+ function recordIsClosed(record, byId, visiting = new Set()) {
42
+ if (!record) return false;
43
+ if (record.status !== "superseded") return CLOSED_STATUSES.has(record.status);
44
+ if (!record.superseded_by || visiting.has(record.confirmation_id)) return false;
45
+ visiting.add(record.confirmation_id);
46
+ const replacement = byId.get(record.superseded_by);
47
+ return Boolean(replacement) && recordIsClosed(replacement, byId, visiting);
48
+ }
49
+
50
+ function detectCycles(records, byId, errors) {
51
+ const visiting = new Set();
52
+ const visited = new Set();
53
+ const visit = (record) => {
54
+ if (visited.has(record.confirmation_id)) return;
55
+ if (visiting.has(record.confirmation_id)) {
56
+ addError(errors, `records.${record.confirmation_id}`, "dependency cycle detected");
57
+ return;
58
+ }
59
+ visiting.add(record.confirmation_id);
60
+ for (const dependencyId of list(record.depends_on)) {
61
+ const dependency = byId.get(dependencyId);
62
+ if (dependency) visit(dependency);
63
+ }
64
+ visiting.delete(record.confirmation_id);
65
+ visited.add(record.confirmation_id);
66
+ };
67
+ records.forEach(visit);
68
+ }
69
+
70
+ export function validateConfirmationSummary(summary, options = {}) {
71
+ const errors = [];
72
+ const warnings = [];
73
+ if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
74
+ return { valid: false, errors: ["summary: must be a mapping"], warnings: [], frontier: [], blocked_proposals: [] };
75
+ }
76
+ if (summary.schema_version !== 1) addError(errors, "schema_version", "must equal 1");
77
+ for (const field of ["summary_id", "node", "iteration_id", "owner", "source_revision", "updated_at"]) {
78
+ if (!text(summary[field])) addError(errors, field, "must be a non-empty string");
79
+ }
80
+ if (!ID_PATTERN.test(String(summary.summary_id || ""))) addError(errors, "summary_id", "has invalid format");
81
+ if (!Number.isInteger(summary.version) || summary.version < 1) addError(errors, "version", "must be a positive integer");
82
+ if (!Number.isInteger(summary.frontier_round) || summary.frontier_round < 1) addError(errors, "frontier_round", "must be a positive integer");
83
+ if (!Array.isArray(summary.records)) addError(errors, "records", "must be an array");
84
+ const records = list(summary.records);
85
+ const byId = new Map();
86
+ records.forEach((record, index) => {
87
+ const prefix = `records[${index}]`;
88
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
89
+ addError(errors, prefix, "must be a mapping");
90
+ return;
91
+ }
92
+ if (!text(record.confirmation_id) || !ID_PATTERN.test(record.confirmation_id || "")) addError(errors, `${prefix}.confirmation_id`, "must be a stable ID");
93
+ if (byId.has(record.confirmation_id)) addError(errors, `${prefix}.confirmation_id`, "must be unique");
94
+ else byId.set(record.confirmation_id, record);
95
+ if (!text(record.question_id) || !ID_PATTERN.test(record.question_id || "")) addError(errors, `${prefix}.question_id`, "must be a stable ID");
96
+ if (!CONFIRMATION_CATEGORIES.has(record.category)) addError(errors, `${prefix}.category`, "is not an allowed category");
97
+ if (!CONFIRMATION_STATUSES.has(record.status)) addError(errors, `${prefix}.status`, "is not an allowed status");
98
+ if (!Number.isInteger(record.frontier_round) || record.frontier_round < 1) addError(errors, `${prefix}.frontier_round`, "must be a positive integer");
99
+ if (!text(record.question)) addError(errors, `${prefix}.question`, "must be provided");
100
+ if (!text(record.recommendation)) addError(errors, `${prefix}.recommendation`, "must be provided");
101
+ if (!Array.isArray(record.depends_on)) addError(errors, `${prefix}.depends_on`, "must be an array");
102
+ if (!Array.isArray(record.evidence_refs)) addError(errors, `${prefix}.evidence_refs`, "must be an array");
103
+ if (!Array.isArray(record.authority_refs)) addError(errors, `${prefix}.authority_refs`, "must be an array");
104
+ if (!NEXT_ACTION_OWNERS.has(record.next_action_owner)) addError(errors, `${prefix}.next_action_owner`, "must be agent, user, or external");
105
+ if (!text(record.next_action)) addError(errors, `${prefix}.next_action`, "must be explicit and idempotent");
106
+ if (record.status === "proposed" && record.decision !== null && record.decision !== undefined) addError(errors, `${prefix}.decision`, "must be null while proposed");
107
+ if (["confirmed", "rejected"].includes(record.status)) {
108
+ if (!text(record.decision)) addError(errors, `${prefix}.decision`, "is required after a decision");
109
+ if (!text(record.actor)) addError(errors, `${prefix}.actor`, "is required after a decision");
110
+ if (!text(record.confirmed_at)) addError(errors, `${prefix}.confirmed_at`, "is required after a decision");
111
+ if (!list(record.authority_refs).length && !options.allowUnlinkedConfirmed) addError(errors, `${prefix}.authority_refs`, "must point to the owning artifact or handoff");
112
+ }
113
+ if (["observed_fact", "execution_fact"].includes(record.category) && !list(record.evidence_refs).length) addError(errors, `${prefix}.evidence_refs`, "is required for an observed fact");
114
+ if (record.status === "deferred" && record.next_action_owner === "agent") addError(errors, `${prefix}.next_action_owner`, "deferred decisions require user or external ownership");
115
+ if (record.status === "superseded" && !text(record.supersedes) && !text(record.superseded_by)) addError(errors, prefix, "must point to a replacement or predecessor");
116
+ });
117
+
118
+ for (const record of records) {
119
+ for (const dependencyId of list(record.depends_on)) {
120
+ if (!byId.has(dependencyId)) addError(errors, `records.${record.confirmation_id}.depends_on`, `unknown dependency ${dependencyId}`);
121
+ if (dependencyId === record.confirmation_id) addError(errors, `records.${record.confirmation_id}.depends_on`, "cannot depend on itself");
122
+ }
123
+ if (record.supersedes && !byId.has(record.supersedes)) addError(errors, `records.${record.confirmation_id}.supersedes`, `unknown record ${record.supersedes}`);
124
+ if (record.superseded_by && !byId.has(record.superseded_by)) addError(errors, `records.${record.confirmation_id}.superseded_by`, `unknown record ${record.superseded_by}`);
125
+ }
126
+ detectCycles(records, byId, errors);
127
+ const frontier = records.filter((record) => record.status === "proposed"
128
+ && list(record.depends_on).every((dependencyId) => recordIsClosed(byId.get(dependencyId), byId)));
129
+ const blockedProposals = records.filter((record) => record.status === "proposed" && !frontier.includes(record));
130
+ if (summary.shared_understanding === true && frontier.length) addError(errors, "shared_understanding", "cannot be true while frontier is non-empty");
131
+ if (summary.shared_understanding === true && blockedProposals.length) addError(errors, "shared_understanding", "cannot be true while a proposed decision is blocked");
132
+ if (summary.shared_understanding === true) {
133
+ if (!text(summary.shared_understanding_actor)) addError(errors, "shared_understanding_actor", "is required after user confirmation");
134
+ if (!text(summary.shared_understanding_confirmed_at)) addError(errors, "shared_understanding_confirmed_at", "is required after user confirmation");
135
+ }
136
+ return {
137
+ valid: errors.length === 0,
138
+ errors,
139
+ warnings,
140
+ frontier: frontier.map((record) => record.confirmation_id),
141
+ blocked_proposals: blockedProposals.map((record) => record.confirmation_id),
142
+ };
143
+ }
144
+
145
+ export function readConfirmationSummary(file, options = {}) {
146
+ const absolute = path.resolve(file);
147
+ const bytes = fs.readFileSync(absolute);
148
+ const summary = parseYaml(bytes.toString("utf8"));
149
+ return {
150
+ file: absolute,
151
+ checksum: crypto.createHash("sha256").update(bytes).digest("hex"),
152
+ summary,
153
+ validation: validateConfirmationSummary(summary, options),
154
+ };
155
+ }
156
+
157
+ export function writeConfirmationSummary(file, summary) {
158
+ const validation = validateConfirmationSummary(summary);
159
+ if (!validation.valid) throw new Error(`Invalid confirmation summary:\n${validation.errors.join("\n")}`);
160
+ const absolute = path.resolve(file);
161
+ if (fs.existsSync(absolute)) {
162
+ const existing = readConfirmationSummary(absolute, { allowUnlinkedConfirmed: false });
163
+ if (existing.summary.summary_id !== summary.summary_id) throw new Error("summary_id cannot change for an existing confirmation summary");
164
+ if (summary.version < existing.summary.version) throw new Error("confirmation summary version cannot move backwards");
165
+ const incoming = new Map(list(summary.records).map((record) => [record.confirmation_id, record]));
166
+ for (const record of list(existing.summary.records)) {
167
+ if (CLOSED_STATUSES.has(record.status) && incoming.has(record.confirmation_id)
168
+ && !sameRecord(record, incoming.get(record.confirmation_id))) {
169
+ throw new Error(`closed confirmation ${record.confirmation_id} is immutable; append a superseding record`);
170
+ }
171
+ }
172
+ if (summary.version === existing.summary.version && sameRecord(existing.summary, summary)) return validation;
173
+ }
174
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
175
+ fs.writeFileSync(absolute, stringifyYaml(summary), "utf8");
176
+ return validation;
177
+ }