Files
yovision/docs/raw/contracts/README.md
T

139 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 事件契约 v0.1(已冻结)
> **冻结日期**:2026-08-03
> **范围**:推理侧(silver_pose 及后续推理内核)→ YoVision 平台侧的唯一数据契约。
> **依据**:《07-事件契约比对-silver_pose.md》
| 文件 | 用途 |
| --- | --- |
| `event-v0.1.schema.json` | JSON Schema(draft 2020-12),规范本体 |
| `event-v0.1.example-current.json` | **现状**基线:silver_pose 今天就能产出的形态,P1 缺口全为 null |
| `event-v0.1.example-target.json` | **目标**形态:P1 补齐后,含 keypoint 序列、clip、自动 outcome |
| `event-v0.1.example-radar.json` | **非成像模态**:卫生间纯雷达跌倒,`snapshot_uris` 为空数组,门磁佐证 |
三个 example 都是**测试夹具**,两侧仓库的单元测试都应对它们做校验。
### 修订记录
| 日期 | 变更 | 理由 |
| --- | --- | --- |
| 2026-08-03 | 初次冻结 | — |
| 2026-08-03 | 新增 `sensors[]` 与 `observation.signal_seq_uri`;`evidence.snapshot_uris` 的 `minItems` 由 1 放宽为 0 | 感知系统后期接入毫米波雷达等非成像设备,原 schema 把「感知=摄像头」焊死在契约里,纯雷达事件无法表达。**本次为原地修订**,理由是当时零实现;首个生产者上线后,§1.2 的版本递增规则绝对生效,不再允许原地改 |
---
## 1. 冻结意味着什么
1. **本目录内容在两个仓库各存一份,内容必须逐字节相同。** 任何一侧修改,同一次提交内同步另一侧
2. `schema_version` 为 `"0.1"`。**破坏性变更必须递增版本并保留旧版本文件**,不得原地修改已冻结的 schema
3. 「破坏性」的定义:删除字段、收紧取值域、把可空改为必填、改变字段语义。新增可空字段与新增 `kind` 取值**不算**破坏性
4. 实验性字段一律放 `ext`,不占用顶层命名空间,也不触发版本升级
## 2. 三条设计决定(及其代价)
**① 所有顶层键必须存在,可为 null,但不得省略。**
「省略」与「显式 null」在下游无法区分,这是这类系统最常见的排查陷阱——`identity_status` 就是被这个问题逼出来的字段。代价是生产者要写一串 null,但生产者只有一个 mapper 函数。
**② 根对象 `additionalProperties: false`。**
未登记字段一律拒绝。防的是推理侧偷偷加字段、平台侧偷偷依赖,几个月后没人知道它是不是契约的一部分。逃生口是 `ext`。
**③ `confidence` 允许为 null,且现阶段必须为 null。**
几何证据 + 时间窗状态机的判定链路没有天然置信度。伪造一个常量会污染所有下游阈值调优。等换成分类模型再填。
## 3. kind 注册表
新增 `kind` 不需要升 schema 版本,但**必须在此表登记**。
| kind | 含义 | 默认 severity | 产出方 | 状态 |
| --- | --- | --- | --- | --- |
| `fall` | 人员摔倒确认 | `high` | silver_pose FSM 进入 CONFIRMED | ✅ 已上线 |
## 4. silver_pose → 契约 映射表
mapper 的完整实现规格。左列出处见《07》§1。
| 契约字段 | 来源 | 变换 |
| --- | --- | --- |
| `schema_version` | — | 常量 `"0.1"` |
| `id` | — | **平台侧**生成 ULID,加 `evt_` 前缀 |
| `source_event_id` | `event_id` | 原样。截图文件即按它命名,是回溯本地证据的唯一钥匙 |
| `tenant_id` / `site_id` / `device_id` | `source_id` | 经平台设备映射表解析。**事件中不得出现 RTSP 地址或凭据** |
| `sensors` | `source_id` | 单摄像头事件为 `[{device_id, "video", "primary"}]`;多传感器融合由推理侧列全 |
| `kind` | `event_id` 的 `FALL-` 前缀 | 提升为显式字段 `"fall"` |
| `severity` | — | 按 kind 查注册表 → `"high"` |
| `confidence` | — | 恒 `null`(见 §2③) |
| `detected_at` | `confirmed_at_utc` | 原样 |
| `occurred_at` | 换算 | `confirmed_at_utc - latency_seconds`。误差为单帧级 |
| `latency_seconds` | `latency_seconds` | 原样 |
| `config_version` | `config_version` | 原样 |
| `rule` | — | `null`(推理侧无规则引擎),平台侧按 kind 反查补全 |
| `subject.class` | — | 常量 `"person"` |
| `subject.track_id` | `track_id` | 原样 |
| `subject.attributes` | — | `{}` |
| `subject.anon_id` / `identity` | — | `null` |
| `subject.identity_status` | — | 常量 `"not_enabled"` |
| `observation.*` | — | 全 `null`(P1 补 keypoint_seq_uri;非成像模态补 signal_seq_uri) |
| `evidence.snapshot_uris` | `screenshot` | 相对路径 → URI,单元素数组。**非成像模态为 `[]`** |
| `evidence.clip_uri` / `clip_range` | — | `null`(P1 补) |
| `dedup_key` / `aggregated_into` | — | `null`,由平台侧构造 |
| `outcome` | `state` | `CONFIRMED` → `"unknown"`;`RECOVERING` 稳定后回传 `"subject_recovered"` + `outcome_source: "auto"` |
| `diagnostics.fsm_state` | `state` | 原样,**仅供排查** |
| `diagnostics.*_monotonic` | 同名字段 | 原样。**单调时钟跨进程无意义,不得用于任何时间计算** |
| `ext` | — | `{}` |
## 5. schema 拦不住的四件事(必须写代码校验)
已实测(`REJECT` = schema 能拦,`ACCEPT` = 拦不住):
schema **能**拦:省略必填键 / 非法 ULID / 顶层多余字段 / outcome 越界 / `config_version` 空串 / `snapshot_uris` 空数组 / 负 latency / kind 大写。
schema **拦不住**,需在 mapper 与平台入口各加一道断言:
| # | 校验 | 理由 |
| --- | --- | --- |
| 1 | `detected_at >= occurred_at` | 跨字段约束,JSON Schema 表达不了。倒序时间会让 SLA 统计出负数 |
| 2 | `\|detected_at - occurred_at - latency_seconds\| < 0.1s` | 三者冗余,必须自洽。不一致说明 mapper 用错了时钟 |
| 3 | `confidence is null`(现阶段) | schema 只能约束 0–1 区间,拦不住伪造值 |
| 4 | 证据文件名不含 IP、端口、凭据、客户名 | 只允许 `事件ID + 日期目录`。文件名会出现在日志、URL 与工单里 —— 这是 silver_pose `alerts.py` 头注释的既有纪律,此处升格为强制校验 |
| 5 | `sensors` 中恰有一个 `role == "primary"`,且其 `device_id == 顶层 device_id` | 数组约束,schema 表达不了。两个 primary 会让归因和去重都失去基准 |
| 6 | 若任一 `sensors[].modality == "video"`,则该 `device_id` 所在区域 `privacy_flag` 必须为 false | 隐私区域硬约束的运行时兜底。设备录入时已拦一道,此处是第二道 |
### 5.1 隐私区域约束的正确表述
原表述「隐私区域拒绝 `device_type = camera`」应改为**按模态判定**:
```
privacy_flag = true 的区域:
允许 modality ∈ {radar, contact, button, wearable}
拒绝 modality ∈ {video}
```
差别不是措辞。按设备类型判定,每接一种新设备就要重新问一次"它算不算摄像头";按模态判定,规则一次写死,新设备只需声明自己的 modality。《01》RQ-S1-05、《02》NFR-CMP-05、《03》§3.2 与落地清单四处的表述需同步。
## 6. 本地校验命令
```bash
pip install 'jsonschema>=4.18'
python3 - <<'PY'
import json
from jsonschema import Draft202012Validator, FormatChecker
s = json.load(open('event-v0.1.schema.json'))
Draft202012Validator.check_schema(s)
v = Draft202012Validator(s, format_checker=FormatChecker())
for f in ('event-v0.1.example-current.json', 'event-v0.1.example-target.json'):
errs = list(v.iter_errors(json.load(open(f))))
print(f, 'OK' if not errs else [e.message for e in errs])
PY
```
已于 2026-08-03 执行通过:schema 合法,两个 example 均校验通过。
## 7. 未决项
| # | 事项 | 决策人 |
| --- | --- | --- |
| 1 | `source_id` → `device_id` 映射表放在哪(平台 DB 表 / 边缘配置文件) | 待定 |
| 2 | 投递通道:HTTP POST 起步还是直接上 ZeroMQ | 待定,建议先 HTTP |
| 3 | `outcome` 回写的反向通道协议(平台 → 推理侧) | 待定 |
| 4 | silver_pose 侧本文件的落地方式(需按其 T-xxx 任务流程走) | 待定 |