2026-08-03 17:22:22 +08:00
|
|
|
|
# 事件契约 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` = 拦不住):
|
|
|
|
|
|
|
2026-08-10 23:53:11 +08:00
|
|
|
|
schema **能**拦:省略必填键 / 非法 ULID / 顶层多余字段 / outcome 越界 / `config_version` 空串 / 负 latency / kind 大写。`snapshot_uris` 空数组对非成像事件是合法值,见修订记录、雷达示例和 schema 的 `minItems: 0`。
|
2026-08-03 17:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
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 任务流程走) | 待定 |
|