feat: add privileged identity outbox observability
Build and Test Quality Gate / Contract Web typecheck and build (pull_request) Successful in 13m17s
Build and Test Quality Gate / Python tests and Contract API migration (pull_request) Failing after 29m27s
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 23m1s

This commit is contained in:
2026-08-03 01:41:34 +08:00
parent 8fcb16b580
commit f32afb363c
20 changed files with 3488 additions and 28 deletions
@@ -683,3 +683,103 @@ P1 发布门槛:五类操作均可在受控运营台完成端到端演练;
- Alembic 代码 head 为 `20260802_0039`。真实 PostgreSQL 专项使用临时 schema 验证不可变触发器,测试结束后清理,不把测试迁移或事件表遗留在公共 schema。
本检查点完成后仍不得把审批状态机扩展为直接身份写入。下一阶段必须以独立设计和验收任务建设统一执行器、固定锁顺序、执行前版本复核、outbox、失败补偿、支持访问在线鉴权/到期/紧急撤销,以及独立回滚预览和双人审批。
## 21. Transactional outbox 最小切片检查点(2026-08-02)
本阶段新增迁移 `20260802_0040` 和 `privileged_identity_outbox_messages`,但不改变身份执行边界。提交、批准、拒绝和过期物化在追加状态事件的同一数据库事务内生成一条 outbox 消息;`(event_id, effect_type)` 与 `idempotency_key` 均有唯一约束,因此操作或审批的幂等重放不会重复排队。
- 迁移不为 `0038/0039` 已存在的历史事件回填 outbox,避免把历史状态重新解释为待投递副作用;投递状态只覆盖 0040 上线后新产生的事件。
- outbox payload 只包含事件/操作 ID、序号、类型、状态、计数、目标指纹、安全 `trace_id`、快照哈希和发生时间,不包含完整快照、操作者、邮箱、姓名、工单引用或正文、凭据、令牌、密码、secret 或原始目标引用;`payload_hash` 可由 payload 重算。
- worker 基座采用 at-least-once 语义:PostgreSQL 使用 `FOR UPDATE SKIP LOCKED` 认领;消息认领后持有有限租约,失败按指数退避重试,达到最大次数进入 `dead_letter`。SQLite 领取使用 `BEGIN IMMEDIATE` 防止意外重复认领,但部署仍只支持单进程、单 worker,不作为横向扩容方案。
- 平台管理员仅可调用 `GET /api/v1/identity/privileged-operations/{operation_id}/delivery-status` 查看消息 ID、事件、状态、尝试次数、稳定错误码和时间;响应不返回 `payload`、`lease_owner` 或错误详情。
- 当前 transport 默认为禁用且不领取消息;没有 SMTP、工单或其他外部副作用,没有人工 `POST retry`,也没有 execute、rollback、merge、split、recover、grant、revoke 或真实 support access 写入口。
- 发布采用先迁移后发代码;代码回滚时保留 outbox 表。执行 `0040` 数据库 downgrade 前必须停止 worker,并确认不存在 queued、processing、retry_scheduled 或 dead-letter 消息,避免丢失投递意图或事故证据。
- SQLite 迁移与治理专项为 `18/18`,包含提交、批准、拒绝、过期故障注入、delivery-status GET 无写副作用和底层并发认领互斥;页面专项 Node `2/2`、Web 全量 Node `34/34`、TypeScript、Python compileall 和 101 路由生产构建已通过。PostgreSQL 专项已在 `192.168.3.90:5432/yuqei_contract` 的隔离临时 schema 中 `1/1` 通过,覆盖 0040 head、事件/outbox 一一对应、并发领取互斥、租约恢复、稳定幂等键、重试和 dead-letter;测试 schema 已精确清理。
下一阶段仍须单独评审 transport 选择、消息消费者幂等契约、告警与死信处置 SOP;启用真实 transport 前,还必须评审客户端可控 operation ID 与无密钥目标指纹的外发关联风险,必要时改用服务端 opaque ID/HMAC 或移除非必要指纹。上述能力不得被解释为身份执行器已经获准开放。
## 22. 全局 outbox 只读可观测与死信排查交付检查点(2026-08-02)
### 22.1 目标与边界
本阶段只补齐平台管理员对 0040 outbox 的全局健康判断和死信排查能力,复用现有 `privileged_identity_outbox_messages`,不新增 `0041` 迁移。所有入口均为只读 `GET`,不得认领、续租、发送、重试、重排、确认或删除消息,也不得物化治理操作过期状态。
- 活跃消息统一指 `queued`、`processing`、`retry_scheduled`。
- 默认超龄阈值为 900 秒;只读查询可通过受限的 `stale_after_seconds` 调整阈值,但不得改变数据库配置或消息状态。
- 临近最大尝试指活跃消息的 `attempt_count >= max_attempts - 1`;过期租约指 `processing` 消息存在租约且 `lease_expires_at` 早于查询时刻。
- 健康分级按最高风险优先:存在任意 `dead_letter` 或过期 processing 租约为 `critical`;否则存在超龄活跃消息或临近最大尝试消息为 `degraded`;其余为 `healthy`。空队列必须为 `healthy`。
- 最老活跃消息年龄在没有活跃消息时返回 `null`,不能伪造为 `0`;所有阈值计算复用同一个 `generated_at` 查询时刻。真实 PostgreSQL 专项已在隔离临时 schema 中验证并发读一致性,以及 overview/dead-letter 两个只读查询前后 operation、approval、event、outbox 全字段快照完全一致。
### 22.2 API 与脱敏契约
使用与单操作详情分离的命名空间,避免和 `{operation_id}` 路由冲突:
- `GET /api/v1/identity/privileged-outbox/overview`
- `GET /api/v1/identity/privileged-outbox/dead-letters`
两个接口仅允许当前仍有效的平台管理员访问;个人用户、企业普通成员、企业管理员和已撤销平台权限的旧会话均返回 `403`,未登录返回 `401`。接口实现不得调用 `_materialize_expired_operations` 或任何 worker claim/transition 方法。
`overview` 至少返回查询时刻、健康级别、各状态计数、活跃总数、最老活跃消息年龄、超龄活跃数、过期 processing 租约数、临近最大尝试数,以及按稳定错误码汇总的 dead-letter 数量。空白或缺失错误码统一归入固定的 `unknown`,不把原始错误详情转换为展示文本。
`dead-letters` 提供稳定分页、稳定错误码过滤和确定性排序。默认按 `updated_at DESC, message_id DESC` 排序,分页参数有明确上限;operation type 和 effect type 作为只读排查上下文返回,本切片不提供对应组合筛选。单项只允许返回:
- message、event、operation ID;
- event type、operation type、effect type;
- 安全 `trace_id`;
- attempt/max attempts;
- 稳定错误码;
- created、updated、last attempt 时间。
两个响应均不得返回 payload、payload hash、idempotency key、lease owner、lease token、原始错误详情、目标指纹、快照或快照哈希、actor、邮箱、姓名、账号/成员身份信息、工单正文、凭据或 secret。列表和聚合也不得通过筛选项、错误文案或导出功能间接泄露这些字段。
### 22.3 只读一致性与验收标准
- [x] 平台管理员访问 overview 和 dead-letters 返回 `200`;普通用户和企业管理员返回 `403`,未登录返回 `401`。
- [x] 空队列返回 `healthy`,所有状态计数为 0,最老活跃消息年龄为 `null`,dead-letter 列表为空页。
- [x] queued、processing、retry_scheduled、过期租约、超龄、临近最大尝试和 dead-letter 的计数与健康优先级符合 22.1,阈值等于边界值时有固定且已测试的包含/排除语义。
- [x] dead-letter 按稳定错误码聚合准确;空错误码只进入 `unknown`,响应不包含原始错误详情。
- [x] dead-letter 分页无重复、无遗漏,稳定错误码过滤准确,时间相同记录仍按 message ID 确定性排序,非法页码、页大小和阈值返回稳定 `422`。
- [x] 对同一数据库连续调用两个 GET 前后,operation、approval、event 和 outbox 的行数、状态、attempt、lease、时间戳、payload 与 hash 完全不变;不得因查询物化过期操作或追加事件/outbox。
- [x] 响应字段与序列化后的 JSON 均通过敏感字段泄露扫描,不出现 payload、幂等键、租约所有者、原始错误、目标指纹或身份信息。
- [x] transport disabled 时全局只读接口仍可用,worker 发送/认领数量保持 0,调用接口不会触发 claim。
- [x] OpenAPI 只新增上述两个 GET,不出现 `POST retry/requeue/ack`,也不出现 execute、rollback、merge、split、recover、grant、revoke 或 support access 身份写路径。
- [x] SQLite 定向测试、Web 定向测试、TypeScript、Python 编译、生产构建和 `git diff --check` 通过。
- [x] PostgreSQL 0040 并发认领、租约恢复和本阶段只读一致性查询已在 `192.168.3.90:5432/yuqei_contract` 的隔离临时 schema 中 `1/1` 通过;测试完成后 schema 已精确清理,未以 SQLite 结果替代。
本地交付证据:治理与迁移专项 `20/20`、反滥用专项 `9/9`、Web 全量 Node `36/36`、页面专项 `4/4`、真实 PostgreSQL 0040/并发/租约/死信/只读一致性专项 `1/1`、TypeScript、Python compileall、单一 Alembic head `20260802_0040`、101 路由生产构建和 `git diff --check` 均通过。运行中 OpenAPI 对两个新路径各注册 1 个 GET,outbox 写方法数量为 0;已登录平台管理员桌面/390px 移动视图加载成功且控制台 error 为 0。Contract API 全量测试未在本检查点复跑,不能用上述定向结果替代。
### 22.4 死信排查 SOP
1. 值班人员只读打开 overview,记录查询时刻、健康级别、dead-letter/过期租约/超龄/临近上限计数和使用的阈值;`critical` 立即建立 P0/P1 事故记录并通知身份治理与平台值班负责人,`degraded` 按 P1 运营异常跟踪。
2. 在 dead-letters 中按稳定错误码、operation type 和 effect type 缩小范围,记录 message/event/operation ID、安全 `trace_id`、attempt/max attempts 与时间,不复制 payload、目标引用、actor 或原始错误到工单。
3. 使用安全 `trace_id` 和三个不透明 ID 在受控日志/审计系统交叉核对部署版本、数据库连接、消费者版本和外部依赖状态;需要查看受限原始日志时必须走既有最小权限与审计流程,不能扩展本页面响应。
4. 判断是否为代码缺陷、配置/依赖故障、租约异常或消费者幂等冲突。禁止直接 `UPDATE/DELETE` outbox、修改 attempt/lease、手工重排、人工调用 transport 或绕过审批执行身份写入。
5. 修复方案通过独立变更评审和回归后发布。当前没有人工 retry/ack 能力,历史 dead-letter 保留为事故证据;若未来需要恢复投递,必须另立设计,定义授权、幂等、审计和回滚并重新通过安全评审。
6. 发布后用 overview 观察新消息是否继续进入异常状态,并在事故记录中附只读查询证据。关闭事故不删除或改写 dead-letter;涉及 PostgreSQL 多 worker 的结论必须引用隔离 schema 并发验收记录。
### 22.5 本阶段明确非目标与发布门禁
- 不新增数据库迁移,不改变 0040 状态机、约束、payload 或历史不回填策略。
- 不增加 `POST retry`、requeue、ack、cancel、delete、lease takeover 或任何人工消息状态写接口。
- 不启用 worker transport,不连接 SMTP、工单、Webhook 或其他外部消费者,不把可观测页面作为 transport 上线批准。
- 不实现消费者业务副作用、身份执行器、支持访问授权、失败补偿或回滚;审批完成仍不代表身份数据已变更。
- 不开放 execute、rollback、merge、split、recover、grant、revoke,不直接写账号、成员、凭据、平台权限或工作空间归属。
- 真实隔离 PostgreSQL 0040 并发验证现已通过,但该结果不等于外部副作用获准上线。完成 transport 选择、消费者幂等契约和外发关联风险评审前,多 worker 与真实 transport 继续阻断。
## 23. Outbox 数据库完整性加固检查点(2026-08-03)
本检查点是 22 节只读可观测交付之后的独立数据库加固阶段。22.5 中“不新增 `0041`”仅描述当时不改变 0040 模型的阶段边界;本阶段根据独立质量复核发现的跨操作事件归属和非法租约组合风险,新增迁移 `20260803_0041`,不回改已经验收的 0040 历史迁移。
- 升级前先只读统计 outbox 与 event 的 operation 归属错配,以及 status/lease 非法组合。任一计数非零时拒绝迁移,不自动修复、删除或重排数据;跨操作错配必须作为审计完整性事件调查。
- 事件表增加唯一索引 `(operation_id, event_id)`;outbox 增加 `(operation_id, event_id)` 复合外键并保持 `ON DELETE NO ACTION`。PostgreSQL 由外键直接拒绝把操作 A 的消息关联到操作 B 的事件;SQLite 默认可能关闭外键执行,因此迁移与自动建库路径同时安装 outbox INSERT/UPDATE 拒绝触发器,不能只依赖外键声明。
- 新增租约 CHECK:`processing` 必须同时具有 `lease_owner` 和 `lease_expires_at`;任何非 `processing` 状态必须同时清空两项。该约束避免 processing 永久滞留且无法被重领/告警,也避免 queued、retry、delivered 或 dead-letter 残留租约。
- SQLite batch-recreate outbox 表并安装两条关联拒绝触发器,事件表只增加唯一索引;0039 的事件 UPDATE/DELETE 拒绝触发器保留。PostgreSQL 增加索引和约束时可能取得表锁,发布时必须短暂停止 Contract API 治理写流量;当前 transport 已关闭,不存在 worker 写流量。
- downgrade 只移除复合外键、租约 CHECK 和事件唯一索引,不删除或改写 operation、approval、event、outbox 数据。若需要回滚,仍应先停止治理写流量并保留审计证据。
验收证据:SQLite 0041 迁移专项 `2/2` 通过,覆盖两类升级前拒绝、复合外键、六种非法租约组合、四种合法租约状态及 downgrade/upgrade;治理与 worker 专项 `19/19` 通过。真实 PostgreSQL 隔离 schema 专项 `1/1` 通过,覆盖 0041 head、约束元数据、跨操作关联拒绝、非法租约拒绝、并发 `SKIP LOCKED`、租约恢复、重试/dead-letter、只读可观测无副作用和 schema 精确清理;组合专项合计 `22/22`。
Contract API 单进程全量回归在 10 分钟命令上限处被终止且未返回失败堆栈,因此不把该次运行写成通过。随后按四个隔离进程覆盖全部 24 个测试模块,收集并执行 `234/234` 项全部通过;各组分别为合同工作流 `91/91`、身份与授权 `83/83`、0041/特权身份/PostgreSQL `22/22`、基础设施与迁移 `38/38`。Python compileall、单一 Alembic head `20260803_0041` 和 `git diff --check` 通过。
本地运行 SQLite 库升级前为 0040,outbox 行数、跨操作事件错配和非法租约计数均为 `0`;停止 Contract API 后已生成 `contract-api-20260802-before-0041-20260803-012502.sqlite3` 备份,再升级到 0041。补充 SQLite 触发器后执行 0041 downgrade/upgrade 循环,确认复合索引、复合外键、租约 CHECK 和两条关联拒绝触发器均存在;Contract API 重启后 `8110` 健康检查返回 `200`,Web `3021` 与 AI Platform `8111` 未重启。
本阶段没有新增或改变任何 API、DTO、Web 页面或 worker transport,不增加人工 retry/requeue/ack,不启用 SMTP/工单/Webhook,不开放 execute、rollback、merge、split、recover、grant、revoke、support access 或其他身份执行入口。审批完成仍只表示治理审批状态,不代表身份数据已变更。
@@ -518,7 +518,7 @@ legal_name: string | null
### 9.28 第十阶段验证证据与后续优先项
- SQLite 治理与迁移专项 `9/9` 通过;Contract API 全量 `220 passed, 1 skipped`,跳过项为需要显式连接参数的 PostgreSQL 专项。该 PostgreSQL 专项随后在 `192.168.3.90:15432` 的唯一临时 schema 中单独执行并 `1/1` 通过。
- SQLite 治理与迁移专项 `9/9` 通过;Contract API 全量 `220 passed, 1 skipped`,跳过项为需要显式连接参数的 PostgreSQL 专项。该 PostgreSQL 专项随后在 `192.168.3.90:5432/yuqei_contract` 的唯一临时 schema 中单独执行并 `1/1` 通过。
- 真实 PostgreSQL 证据覆盖:并发相同操作只创建 1 条操作和 1 条提交事件;同一审批人并发只创建 1 条审批和 1 条决定事件;两个不同审批人并发形成两条有序事件并最终进入 `approved`;拒绝保持终态;过期只物化一次;事件序号、`trace_id`、快照哈希和脱敏规则均通过;最后有效平台管理员撤销预览继续被阻断。
- PostgreSQL 测试按阶段运行 Alembic 到单一 head `20260802_0038`,仅在 `pg_trgm` 索引迁移期间临时把 `public` 加入 `search_path`,其余迁移和运行时连接只使用测试 schema。夹具在 `finally` 中精确删除测试 schema。诊断试跑曾只推进公共 Alembic 标记而未创建事件表;已恢复公共标记为原基线 `20260802_0037`,确认公共事件表不存在、残留治理/诊断 schema 数量为 `0`。
- Web 治理定向回归 `28/28`、TypeScript 和 Python 编译通过;Next.js 生产构建成功生成 `101` 个页面/路由;`git diff --check` 无错误,仅有既有 LF/CRLF 提示。Alembic 当前代码 head 为 `20260802_0038`。
@@ -531,3 +531,56 @@ legal_name: string | null
- 独立质量复核后新增迁移 `20260802_0039`:SQLite 使用两个拒绝触发器、PostgreSQL 使用单一 `BEFORE UPDATE OR DELETE` 触发器,数据库层直接修改或删除治理事件均失败;本地自动建库路径同步安装等价保护。列表与详情也统一映射过期物化等治理异常,避免极端并发冲突退化为 `500`。
- 最终本地复核新增执行 Web 全部 `*.test.mjs`,结果 `34/34`;治理迁移、不可变触发器、错误映射与操作专项在隔离内存数据库下 `10/10`;TypeScript、Python 编译、101 路由 Next.js 生产构建和 `git diff --check` 通过。Web `3021`、Contract API `8110`、AI Platform `8111` 均返回 `200`,Alembic 代码为单一 head `20260802_0039`。
- 本阶段交付结论不变:追加式事件、追踪、脱敏快照和真实 PostgreSQL 并发门槛已完成;身份执行器仍为关闭状态。下一阶段不得直接开放破坏性写入口,必须先单独评审统一执行器、固定锁顺序、版本复核、outbox、失败补偿、支持访问在线鉴权和独立回滚治理。
### 9.30 第十一阶段 transactional outbox 最小切片(2026-08-02)
- 新增 `20260802_0040` 加法迁移、ORM 和 `privileged_identity_outbox_messages`。操作与事件外键均为 `NO ACTION`;状态、尝试次数、唯一事件/effect 和唯一幂等键由数据库约束,投递扫描与操作详情均有索引。
- 0040 不回填 0038/0039 的历史事件,避免旧治理状态在部署后被重新投递;投递状态从升级后新事件开始记录。
- 提交、批准、拒绝和过期事件与脱敏 outbox 消息在同一事务写入。相同操作或审批幂等重放不新增事件或消息;消息 payload 不包含原始身份目标、操作者信息或凭据类字段。
- worker repository 已具备 PostgreSQL `FOR UPDATE SKIP LOCKED`、租约、过期重领、指数退避、最大重试和 `dead_letter` 基座;SQLite 使用 `BEGIN IMMEDIATE` 防重复领取且部署明确限制单进程单 worker。当前未接任何外部 transport。
- 平台管理员详情新增只读 `delivery-status`,前端只展示投递状态和尝试次数;API 不返回 payload、lease owner 或错误详情,且未增加发送、重试或执行控件。
- 回归门槛:SQLite 治理与迁移专项 `18/18`(包含底层并发认领互斥)、页面专项 Node `2/2`、Web 全量 Node `34/34`、TypeScript、Python compileall 和 101 路由生产构建已通过。PostgreSQL 专项已在 `192.168.3.90:5432/yuqei_contract` 的隔离临时 schema 中 `1/1` 通过,校验 0040 head、outbox/事件数量一致、并发认领不重复、租约恢复、稳定幂等键、重试和 dead-letter;测试 schema 已精确清理。
- 身份执行器继续关闭;execute、rollback、merge、split、recover、grant、revoke、真实 support access 写入及人工 outbox retry 仍属禁止范围。
### 9.31 第十二阶段:全局 outbox 只读可观测与死信排查(已交付,2026-08-02)
本阶段优先级为 **P1 运营可观测**,仅复用 0040 数据模型,不新增迁移。目标是让有效平台管理员在不触发任何数据库写入、消息认领或外部副作用的前提下判断 outbox 全局健康并定位 dead-letter;它不是 transport、人工重试或身份执行器的发布阶段。
可执行 Backlog:
1. **P1-OBS-1 后端只读聚合(完成)**:已增加状态计数、活跃总数、最老活跃消息年龄、超龄活跃数、过期 processing 租约数、临近最大尝试数和 dead-letter 稳定错误码聚合。活跃状态固定为 `queued/processing/retry_scheduled`,默认超龄阈值 900 秒。
2. **P1-OBS-2 独立只读 API(完成)**:已增加 `GET /api/v1/identity/privileged-outbox/overview` 和 `GET /api/v1/identity/privileged-outbox/dead-letters`;只允许实时有效的平台管理员,禁止调用过期物化、claim、retry 或状态迁移方法。
3. **P1-OBS-3 死信分页排查(完成)**:已按 `updated_at DESC, message_id DESC` 稳定排序,支持稳定错误码过滤和有上限的分页;operation type、effect type 作为只读上下文返回,本切片未增加对应组合筛选。响应只返回不透明关联 ID、安全 `trace_id`、类型、尝试次数、稳定错误码和时间字段。
4. **P1-OBS-4 前端只读运营区(完成)**:特权身份治理页已增加全局投递健康摘要和 dead-letter 排查区,覆盖 loading、healthy/degraded/critical、空队列、空筛选结果、分页和后端失败状态;不存在 retry、ack、requeue、send、execute 或其他消息/身份动作按钮。
5. **P1-OBS-5 回归与泄露门禁(完成)**:已覆盖平台管理员 `200`、个人/企业管理员 `403`、空队列、阈值边界、健康优先级、聚合、稳定分页/过滤/排序、GET 前后数据库完全不变、禁用 transport 不 claim、OpenAPI 破坏性路径扫描、S3 反滥用策略和响应敏感字段扫描。
6. **P1-OBS-6 PostgreSQL 证据补齐(完成)**:已在 `192.168.3.90:5432/yuqei_contract` 的隔离临时 schema 中复跑并 `1/1` 通过,覆盖 0040 迁移、并发认领、租约恢复、重试/dead-letter、稳定错误码聚合/过滤,以及 overview/dead-letter 查询前后 operation、approval、event、outbox 全字段快照完全一致;测试后已精确清理 schema。
本地验收结果:治理与迁移专项 `20/20`、反滥用专项 `9/9`、Web 全量 Node `36/36`、页面专项 `4/4`、真实 PostgreSQL 0040/并发/租约/死信/只读一致性专项 `1/1`、TypeScript、Python compileall、单一 Alembic head `20260802_0040`、101 路由生产构建及 `git diff --check` 通过;运行时两个新 GET 各注册 1 条,outbox 写方法数量为 0,桌面与 390px 移动视图加载成功且浏览器控制台 error 为 0。Contract API 全量测试未在本检查点复跑,仍作为独立发布证据缺口保留。
验收标准:
- 空队列为 `healthy`;存在 dead letter 或过期 processing 租约为 `critical`;否则存在超龄活跃消息或临近最大尝试消息为 `degraded`;其余为 `healthy`。
- 两个 GET 前后 operation、approval、event、outbox 的行数、状态、attempt、lease、时间戳、payload 和 hash 完全不变;实现不得调用 `_materialize_expired_operations`。
- 响应不得包含 payload、payload hash、idempotency key、lease owner/token、原始错误详情、目标指纹、快照/哈希、actor、邮箱、姓名、账号/成员身份、工单正文、凭据或 secret;空错误码聚合为固定 `unknown`。
- dead-letter 分页无重复或遗漏,组合过滤准确,同时间记录排序确定;非法分页和阈值参数返回稳定 `422`。
- transport disabled 时发送和认领数量继续为 0;OpenAPI 只新增两个 GET,破坏性身份写路径和人工 outbox 写路径数量均为 0。
- 后端和 Web 定向测试、TypeScript、Python 编译、生产构建及 `git diff --check` 通过;PostgreSQL 专项必须单列真实结果,本阶段真实结果为 `1/1` 通过且测试 schema 已清理,未以 SQLite 结果代替。
运营 SOP:先在 overview 记录查询时刻、阈值和风险计数,再在 dead-letters 按稳定错误码及类型过滤,使用安全 `trace_id` 和不透明 ID 在受控日志中关联部署/消费者/依赖证据。不得把 payload、原始目标、actor 或错误详情复制到普通工单,不得直接修改 outbox 或人工发送。`critical` 立即升级身份治理与平台值班负责人;修复必须走独立代码/配置变更评审,历史 dead-letter 继续保留为证据,当前阶段不提供 retry/ack。
明确非目标:不新增 `0041`,不回填历史事件,不启用 transport,不接 SMTP/工单/Webhook,不新增 POST retry/requeue/ack,不开放身份执行器、支持访问或 execute/rollback/merge/split/recover/grant/revoke。真实 PostgreSQL 0040 并发验证虽已完成,但在 transport 选择、消费者幂等契约和外发关联风险评审完成前,仍不得推进多 worker 或真实副作用投递。
### 9.32 第十三阶段:outbox 数据完整性加固(已交付,2026-08-03)
9.31 的“不新增 `0041`”是只读可观测阶段的范围约束。本阶段根据独立质量复核结果,以新的加法迁移 `20260803_0041` 单独处理两个数据库完整性缺口,不修改 0040 历史迁移,也不扩大产品能力。
1. **P1-INT-1 事件归属复合约束(完成)**:事件表增加唯一 `(operation_id, event_id)` 索引;outbox 增加同列复合外键并使用 `ON DELETE NO ACTION`。SQLite 默认可能关闭外键执行,迁移与自动建库路径另安装 INSERT/UPDATE 拒绝触发器;PostgreSQL 由复合外键执行。两种数据库均拒绝跨 operation/event 拼接,避免详情、死信上下文和未来消费者看到错误审计归属。
2. **P1-INT-2 租约状态约束(完成)**:`processing` 必须同时存在 lease owner/expiry,其他状态必须同时清空两项。暂不扩大到 delivered timestamp、last attempt 或 error code 状态机,避免在未完成设计前增加额外语义。
3. **P1-INT-3 升级预检与回滚(完成)**:0041 升级最先只读统计跨操作错配和非法租约行;任一计数非零立即拒绝,不自动修复、删除或重排。downgrade 只移除本阶段约束和索引,不改写业务数据。
4. **P1-INT-4 SQLite/PostgreSQL 一致性(完成)**:SQLite batch-recreate outbox 并安装关联拒绝触发器,0039 事件不可变触发器继续有效;PostgreSQL 使用复合外键与 CHECK。发布时须短暂停止治理写流量,避免索引/约束验证锁与新治理提交竞争。
5. **P1-INT-5 自动化证据(完成)**:SQLite 迁移 `2/2`、治理/worker `19/19`、真实 PostgreSQL `1/1`,组合专项 `22/22`。真实 PostgreSQL 覆盖 0041 head、约束元数据、非法关联/租约拒绝、并发 claim、租约恢复、重试/dead-letter、只读快照不变和隔离 schema 清理。
6. **P1-INT-6 全模块回归(完成)**:Contract API 单进程运行在 10 分钟命令上限处终止且无失败堆栈,不据此宣称通过;随后四个隔离进程覆盖全部 24 个测试模块,收集并执行 `234/234` 项全部通过。分组结果为合同工作流 `91/91`、身份与授权 `83/83`、0041/特权身份/PostgreSQL `22/22`、基础设施与迁移 `38/38`。
当前单一 Alembic head 为 `20260803_0041`;Python compileall 和 `git diff --check` 通过。本阶段不新增 API、DTO、Web 或 transport 逻辑,身份执行器、真实 support access、人工 outbox retry 及 execute/rollback/merge/split/recover/grant/revoke 继续关闭。下一阶段仍须先完成 transport 选择、消费者幂等契约与外发关联风险评审,不得把数据库完整性加固解释为真实副作用或身份写入获准上线。
本地运行库已在停服备份后由 0040 升级到 0041;升级前 outbox、跨操作错配和非法租约计数均为 `0`,备份文件为 `.runtime/db-backups/contract-api-20260802-before-0041-20260803-012502.sqlite3`。补充 SQLite 触发器后已完成 0041 downgrade/upgrade 循环,复合索引、复合外键、租约 CHECK 和两条关联拒绝触发器复核通过;Contract API 重启并在 `8110` 返回 `200`,Web `3021` 与 AI Platform `8111` 保持运行,现有 Web 会话未因本次迁移重启。