--- id: T-015 title: 建立 Bell 事件 v0.1 校验与不可变 PostgreSQL 存储 phase: 3 deps: [T-014] status: TODO created: 2026-08-10 issue: null context_ref: null claim_branch: null work_branch: null write_paths: - docs/tasks/T-015.md - Bell/ - deploy/postgres/012_bell_events.sql - deploy/postgres/013_privileges_bell_events.sql - deploy/postgres/tests/assertions.sql - deploy/postgres/README.md - scripts/test_postgres.ps1 - tests/test_bell_event_contract.py - docs/raw/contracts/README.md - docs/00-ai-start-here.md - docs/03-tech-stack.md - docs/04-architecture.md - docs/06-tasks.md - docs/api.md - docs/current-state.md - AGENTS.md - init.ps1 - init.sh --- ## 问题 / 背景 M3 要求 Bell 首次真实消费冻结的事件契约 v0.1,生成平台 ULID,并把事件事实保存为不可变记录。当前 Bell 只有目录占位,PostgreSQL 也只有 Site/Area 投影源表;如果先做规则或 Alert,会把未经校验、可被覆盖的事件数据变成后续状态机的错误基础。 冻结契约中还存在一处文档自相矛盾:`docs/raw/contracts/README.md` 的修订记录、雷达示例和 JSON Schema 均允许非成像事件的 `snapshot_uris=[]`,但 §5 的旧句仍把空数组列为 schema 可拒绝项。本任务先把该句校正为现有冻结事实,不修改 schema 或字段语义。 ## 关联需求与交互(如适用) - 用户故事:无直接 UI;为 US-003、US-004、US-006 的后续事件/处置流程提供数据基础。 - 交互清单:不适用;本任务不实现 Bell Web/H5 页面或公共查询 API。 - 相关页面 / 路由:不适用。 ## 方案 1. 在 `Bell/` 建立独立 Go module,复用项目冻结的 Go 1.26.5、PostgreSQL 17.10 和 `pgx/v5 v5.10.0`;冻结 Draft 2020-12 校验器与 ULID 库版本、许可证和退出路线,不引入前端或消息总线。 2. 把冻结 schema 逐字节复制到 `Bell/contracts/event-v0.1.schema.json` 并以测试阻止漂移。Bell 内部 ingest factory 接收不含平台 `id` 的候选事实,拒绝上游自报 `id`,由 Bell 生成 `evt_` ULID 后再执行 schema 与代码级断言。Brain→Bell 的 HTTP/消息总线 transport 继续待定,本任务不借内部 Go 类型冻结公共网络协议。 3. 代码级校验覆盖时间顺序/延迟自洽、`confidence=null`、证据 URI 脱敏、唯一 primary 且与顶层设备一致,以及视频隐私准入。隐私与客户敏感名称通过必需 port 注入;解析/映射不可用时失败关闭,不使用 allow-all 生产默认值。 4. 新增 Bell event repository。`bell.events` 保存最终规范化 JSON、摘要和可查询的最小索引字段;平台 ID 冲突时仅允许“同 ID + 同摘要”的幂等重放,不同摘要稳定冲突。后续 outcome 写入独立的 append-only `bell.event_outcomes`,不得更新原事件事实。 5. migration 增加独立 NOLOGIN `bell_runtime` 最小权限角色。migration owner `bell_app` 持有对象;运行时只获得必需的 `SELECT/INSERT`,明确没有事件表的 `UPDATE/DELETE/TRUNCATE`,并以数据库 trigger 拒绝事实改写。生产/共享实例仍由管理员离线 migration,不由 Bell 高权限自迁移。 6. 扩展隔离 PostgreSQL 17.10 harness,使用 `bell_runtime` 登录运行真实 repository/权限/重放/不可变性集成测试;同步根启动入口、技术栈、架构、API 状态和当前仓库现实。 ## 不可变约束 - 阈值 / 数值边界:请求体/单事件规范化 JSON 上限 1 MiB;延迟自洽误差严格小于 100 ms;事件列表/公共 API 不在本任务内;任何默认 16 路值不成为事件数量上限。 - 判定式 / 状态转换:Bell 是 `evt_` ULID 唯一生成者;候选事实不得携带 `id`;最终事实必须同时通过 Draft 2020-12 schema 和 README 六项代码级断言。相同平台 ID/相同摘要为幂等成功,相同 ID/不同摘要为冲突;原事件永不 UPDATE/DELETE,后续 outcome 只能追加。 - 安全边界:未知顶层字段拒绝;视频隐私映射或敏感名称策略不可用时失败关闭;普通错误和测试证据不回显事件全文、证据 URI、客户名、凭据或内部堆栈;`bell_runtime` 不拥有 migration 对象且没有事件事实改写权限。 - 既有契约:`docs/raw/contracts/event-v0.1.schema.json` 字段、枚举与 v0.1 语义不变;只修正文档中与 schema/示例冲突的旧句。Brain transport、规则、Alert、证据回捞、RBAC/JWT/OIDC 和事件公共 API 均另立任务。 ## 验收要点 - 任务相关验证:`go -C Bell test ./...`、`go -C Bell vet ./...`、`go -C Bell build ./...`;`python -m unittest discover -s tests -p "test_bell_event_contract.py" -v`;验证三个冻结示例、负向 schema/六断言、ULID 所有权、幂等冲突和 append-only outcome。 - 完整门禁:因命中 Bell 脚手架、PostgreSQL migration、权限、根启动入口和冻结契约消费者,运行 `./scripts/test_postgres.ps1 -PgRoot D:\pgsql17`、`./init.ps1`、三条 Python 治理命令和 `git diff --check`。隔离测试必须证明现有 5432 listener 前后不变并自动清理临时集群。 - 人工 / 设备验收:不适用;本任务不依赖摄像头、客户网络、GPU、MinIO 或产品 UI。生产上线仍受法务/客户留存政策和 M3 现场 dry-run 门禁约束。 - 构建产物:`go -C Bell build ./...` 可构建全部 Bell 包;不生成可部署 Bell API 二进制,因为 Brain transport、认证和公共 API 尚未冻结。 ## 边界(不改什么) - 不实现 Brain mapper/投递、HTTP 或消息总线事件入口、规则引擎、Alert、ack、升级链、对象存储、证据切片、反馈回流、Bell Web/H5 或 JWT/OIDC。 - 不修改冻结事件 schema、示例字段或 Sense 业务代码;不把整数事件契约 ID 与现有文本逻辑 ID 的待定映射悄悄固化为跨系统规则。 - 不把 `bell_app` owner 凭据交给运行进程,不在仓库保存 DSN、密钥、客户数据、真实事件或真实证据 URI。 ## 协作约束 - 责任 Agent:codex - 唯一写入者:codex - 委派:不启用。 - Gitea:Issue、`context_ref`、claim 与工作分支在双向映射及 dispatcher 分配后回填。 任何新增写路径先检查与其他活跃任务是否重叠;同一时刻只有一个 Agent 修改本任务的 `write_paths`。 ## 执行记录 - 2026-08-10:按 M3 建议拆出本任务;完成仓库/远端状态检查和 `./init.ps1` 基线,现有 58 个 Python 测试及 Sense generate/test/vet/build 全部通过。实现尚未开始。