fix: 统一源配置契约字段命名 (#148)
This commit is contained in:
@@ -15,11 +15,11 @@
|
|||||||
## 消费规则
|
## 消费规则
|
||||||
|
|
||||||
1. 先按 JSON Schema 校验,再执行跨字段语义校验。
|
1. 先按 JSON Schema 校验,再执行跨字段语义校验。
|
||||||
2. `schema` 必须精确等于 `yovision.source-config/v1`;未知主版本不得降级猜测。
|
2. `schema_version` 必须精确等于 `yovision.source-config/v1`;未知主版本不得降级猜测。
|
||||||
3. `ruleSet.profileBinding` 必须与 `profile.id/width/height` 完全一致。
|
3. `rule_set.profile_binding` 必须与 `profile.id/width/height` 完全一致。
|
||||||
4. `ruleSet.state != active` 时不得运行任何规则;`recalibration_required` 表示 Profile 规格变化后需重新标定。
|
4. `rule_set.state != active` 时不得运行任何规则;`recalibration_required` 表示 Profile 规格变化后需重新标定。
|
||||||
5. `areas` 与 `directionalLines` 的 `id` 在同一快照内必须全局唯一;多边形必须非退化,线段起终点不得相同。
|
5. `areas` 与 `directional_lines` 的 `id` 在同一快照内必须全局唯一;多边形必须非退化,线段起终点不得相同。
|
||||||
6. `effectiveAt` 不得早于 `publishedAt`。
|
6. `effective_at` 不得早于 `published_at`。
|
||||||
7. `integrity.value` 是移除顶层 `integrity` 后,对 RFC 8785 JCS 规范化 JSON 字节计算的 SHA-256 小写十六进制摘要。生产消费者应使用合规 JCS 实现;仓库样例只使用 JCS 简单类型子集。
|
7. `integrity.value` 是移除顶层 `integrity` 后,对 RFC 8785 JCS 规范化 JSON 字节计算的 SHA-256 小写十六进制摘要。生产消费者应使用合规 JCS 实现;仓库样例只使用 JCS 简单类型子集。
|
||||||
|
|
||||||
`media.ref` 是 connector 解析的无凭据不透明引用,固定以 `media:` 开头。它不能包含 URI authority、用户名、密码、查询参数、fragment、Windows 盘符或文件系统路径。RTSP 凭据交换与机器身份不属于本契约。
|
`media.ref` 是 connector 解析的无凭据不透明引用,固定以 `media:` 开头。它不能包含 URI authority、用户名、密码、查询参数、fragment、Windows 盘符或文件系统路径。RTSP 凭据交换与机器身份不属于本契约。
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
## 兼容规则
|
## 兼容规则
|
||||||
|
|
||||||
- v1 发布后只允许在预留的顶层 `extensions` 对象中增加命名空间化、非秘密的可选扩展。消费者必须忽略自己不认识的扩展命名空间,但仍须拒绝当前 Schema 或语义规则标记为非法的输入;发布扩展时应同步生产者/消费者测试。v1 核心对象保持封闭,不能通过新增核心字段规避新主版本。
|
- v1 发布后只允许在预留的顶层 `extensions` 对象中增加命名空间化、非秘密的可选扩展。消费者必须忽略自己不认识的扩展命名空间,但仍须拒绝当前 Schema 或语义规则标记为非法的输入;发布扩展时应同步生产者/消费者测试。v1 核心对象保持封闭,不能通过新增核心字段规避新主版本。
|
||||||
- 删除字段、把可选改为必填、收紧已发布取值范围,或改变字段类型、单位、坐标系、Profile 绑定、revision、状态及媒体引用语义,均为破坏性变化,必须发布新主版本目录和新的 `schema` 值。
|
- 删除字段、把可选改为必填、收紧已发布取值范围,或改变字段类型、单位、坐标系、Profile 绑定、revision、状态及媒体引用语义,均为破坏性变化,必须发布新主版本目录和新的 `schema_version` 值。
|
||||||
- 未知主版本必须安全拒绝并保留最后一个已验证配置。不得把未知版本转换成 v1,也不得继续启用来自未知版本的规则。
|
- 未知主版本必须安全拒绝并保留最后一个已验证配置。不得把未知版本转换成 v1,也不得继续启用来自未知版本的规则。
|
||||||
- v1 的坐标始终是相对于 `profile.width × profile.height` 图像平面的 0–1 归一化坐标;原点在左上,x 向右、y 向下。该语义不得在 v1 内改变。
|
- v1 的坐标始终是相对于 `profile.width × profile.height` 图像平面的 0–1 归一化坐标;原点在左上,x 向右、y 向下。该语义不得在 v1 内改变。
|
||||||
|
|
||||||
## revision 与生效
|
## revision 与生效
|
||||||
|
|
||||||
- `(configId, revision)` 唯一标识一个不可变快照;同一 `configId` 的新发布必须使用严格递增的 `revision`。
|
- `(config_id, revision)` 唯一标识一个不可变快照;同一 `config_id` 的新发布必须使用严格递增的 `revision`。
|
||||||
- 消费者仅在 Schema、语义和完整性均通过后,按 `effectiveAt` 原子切换整个快照。重复收到同一 revision 应幂等处理;更小 revision 应拒绝为陈旧配置。
|
- 消费者仅在 Schema、语义和完整性均通过后,按 `effective_at` 原子切换整个快照。重复收到同一 revision 应幂等处理;更小 revision 应拒绝为陈旧配置。
|
||||||
- Profile ID、分辨率或编码变化时,生产者必须发布新 revision。已有几何尚未按新 Profile 校准时,必须设置 `ruleSet.state = recalibration_required`;消费者不得启用其中规则。
|
- Profile ID、分辨率或编码变化时,生产者必须发布新 revision。已有几何尚未按新 Profile 校准时,必须设置 `rule_set.state = recalibration_required`;消费者不得启用其中规则。
|
||||||
- 新 revision 校验失败或未到生效时间时,消费者保留上一份已验证且仍有效的 active revision。
|
- 新 revision 校验失败或未到生效时间时,消费者保留上一份已验证且仍有效的 active revision。
|
||||||
|
|
||||||
## 支持周期
|
## 支持周期
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
## 回退
|
## 回退
|
||||||
|
|
||||||
1. 停止分发有问题的新主版本或新 revision。
|
1. 停止分发有问题的新主版本或新 revision。
|
||||||
2. 重新发布上一主版本的最后一个已验证快照;若仍为同一 `configId`,必须使用该主版本下新的、更大 revision,不能覆盖历史 revision。
|
2. 重新发布上一主版本的最后一个已验证快照;若仍为同一 `config_id`,必须使用该主版本下新的、更大 revision,不能覆盖历史 revision。
|
||||||
3. Brain 通过完整 Schema、语义和摘要校验后原子切回;切换前继续使用最后一个有效快照,或在没有有效快照时保持规则停用。
|
3. Brain 通过完整 Schema、语义和摘要校验后原子切回;切换前继续使用最后一个有效快照,或在没有有效快照时保持规则停用。
|
||||||
4. 记录失败版本和拒绝原因,但不得记录媒体凭据或完整客户配置。
|
4. 记录失败版本和拒绝原因,但不得记录媒体凭据或完整客户配置。
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"schema": "yovision.source-config/v1",
|
"schema_version": "yovision.source-config/v1",
|
||||||
"configId": "school-east-entry-01",
|
"config_id": "school-east-entry-01",
|
||||||
"revision": 7,
|
"revision": 7,
|
||||||
"publishedAt": "2026-08-31T00:10:00Z",
|
"published_at": "2026-08-31T00:10:00Z",
|
||||||
"effectiveAt": "2026-08-31T00:15:00Z",
|
"effective_at": "2026-08-31T00:15:00Z",
|
||||||
"site": {"id": "site-east"},
|
"site": {"id": "site-east"},
|
||||||
"logicalDevice": {"id": "entry-camera-01"},
|
"logical_device": {"id": "entry-camera-01"},
|
||||||
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frameRate": 25},
|
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frame_rate": 25},
|
||||||
"media": {"ref": "media:site-east/entry-01/main", "transport": "rtsp"},
|
"media": {"ref": "media:site-east/entry-01/main", "transport": "rtsp"},
|
||||||
"ruleSet": {"version": "entry-rules-7", "state": "active", "profileBinding": {"profileId": "main-stream", "width": 1920, "height": 1080}, "areas": [{"id": "bad-area", "version": 1, "kind": "danger_area", "enabled": true, "points": [{"x": 0, "y": 0}, {"x": 1.2, "y": 0}, {"x": 0, "y": 1}]}], "directionalLines": []},
|
"rule_set": {"version": "entry-rules-7", "state": "active", "profile_binding": {"profile_id": "main-stream", "width": 1920, "height": 1080}, "areas": [{"id": "bad-area", "version": 1, "kind": "danger_area", "enabled": true, "points": [{"x": 0, "y": 0}, {"x": 1.2, "y": 0}, {"x": 0, "y": 1}]}], "directional_lines": []},
|
||||||
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"schema": "yovision.source-config/v1",
|
"schema_version": "yovision.source-config/v1",
|
||||||
"configId": "school-east-entry-01",
|
"config_id": "school-east-entry-01",
|
||||||
"revision": 7,
|
"revision": 7,
|
||||||
"publishedAt": "2026-08-31T00:10:00Z",
|
"published_at": "2026-08-31T00:10:00Z",
|
||||||
"effectiveAt": "2026-08-31T00:15:00Z",
|
"effective_at": "2026-08-31T00:15:00Z",
|
||||||
"site": {"id": "site-east"},
|
"site": {"id": "site-east"},
|
||||||
"logicalDevice": {"id": "entry-camera-01"},
|
"logical_device": {"id": "entry-camera-01"},
|
||||||
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frameRate": 25},
|
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frame_rate": 25},
|
||||||
"media": {"ref": "media:site-east/entry-01/main", "transport": "rtsp", "password": null},
|
"media": {"ref": "media:site-east/entry-01/main", "transport": "rtsp", "password": null},
|
||||||
"ruleSet": {"version": "entry-rules-7", "state": "active", "profileBinding": {"profileId": "main-stream", "width": 1920, "height": 1080}, "areas": [], "directionalLines": []},
|
"rule_set": {"version": "entry-rules-7", "state": "active", "profile_binding": {"profile_id": "main-stream", "width": 1920, "height": 1080}, "areas": [], "directional_lines": []},
|
||||||
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"schema": "yovision.source-config/v1",
|
"schema_version": "yovision.source-config/v1",
|
||||||
"configId": "school-east-entry-01",
|
"config_id": "school-east-entry-01",
|
||||||
"revision": 7,
|
"revision": 7,
|
||||||
"publishedAt": "2026-08-31T00:10:00Z",
|
"published_at": "2026-08-31T00:10:00Z",
|
||||||
"effectiveAt": "2026-08-31T00:15:00Z",
|
"effective_at": "2026-08-31T00:15:00Z",
|
||||||
"site": {"id": "site-east"},
|
"site": {"id": "site-east"},
|
||||||
"logicalDevice": {"id": "entry-camera-01"},
|
"logical_device": {"id": "entry-camera-01"},
|
||||||
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frameRate": 25},
|
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frame_rate": 25},
|
||||||
"media": {"ref": "C:\\customers\\school-east\\camera-01", "transport": "rtsp"},
|
"media": {"ref": "C:\\customers\\school-east\\camera-01", "transport": "rtsp"},
|
||||||
"ruleSet": {"version": "entry-rules-7", "state": "active", "profileBinding": {"profileId": "main-stream", "width": 1920, "height": 1080}, "areas": [], "directionalLines": []},
|
"rule_set": {"version": "entry-rules-7", "state": "active", "profile_binding": {"profile_id": "main-stream", "width": 1920, "height": 1080}, "areas": [], "directional_lines": []},
|
||||||
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"schema": "yovision.source-config/v1",
|
"schema_version": "yovision.source-config/v1",
|
||||||
"configId": "school-east-entry-01",
|
"config_id": "school-east-entry-01",
|
||||||
"revision": 7,
|
"revision": 7,
|
||||||
"publishedAt": "2026-08-31T00:10:00Z",
|
"published_at": "2026-08-31T00:10:00Z",
|
||||||
"effectiveAt": "2026-08-31T00:15:00Z",
|
"effective_at": "2026-08-31T00:15:00Z",
|
||||||
"site": {"id": "site-east"},
|
"site": {"id": "site-east"},
|
||||||
"logicalDevice": {"id": "entry-camera-01"},
|
"logical_device": {"id": "entry-camera-01"},
|
||||||
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frameRate": 25},
|
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frame_rate": 25},
|
||||||
"media": {"ref": "media:site-east/entry-01/main", "transport": "rtsp"},
|
"media": {"ref": "media:site-east/entry-01/main", "transport": "rtsp"},
|
||||||
"ruleSet": {"version": "entry-rules-7", "state": "active", "profileBinding": {"profileId": "main-stream", "width": 1280, "height": 720}, "areas": [], "directionalLines": []},
|
"rule_set": {"version": "entry-rules-7", "state": "active", "profile_binding": {"profile_id": "main-stream", "width": 1280, "height": 720}, "areas": [], "directional_lines": []},
|
||||||
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"schema": "yovision.source-config/v1",
|
"schema_version": "yovision.source-config/v1",
|
||||||
"configId": "school-east-entry-01",
|
"config_id": "school-east-entry-01",
|
||||||
"revision": 7,
|
"revision": 7,
|
||||||
"publishedAt": "2026-08-31T00:10:00Z",
|
"published_at": "2026-08-31T00:10:00Z",
|
||||||
"effectiveAt": "2026-08-31T00:15:00Z",
|
"effective_at": "2026-08-31T00:15:00Z",
|
||||||
"site": {"id": "site-east"},
|
"site": {"id": "site-east"},
|
||||||
"logicalDevice": {"id": "entry-camera-01"},
|
"logical_device": {"id": "entry-camera-01"},
|
||||||
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frameRate": 25},
|
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frame_rate": 25},
|
||||||
"media": {"ref": "media:site-east/entry-01/main?token=", "transport": "rtsp"},
|
"media": {"ref": "media:site-east/entry-01/main?token=", "transport": "rtsp"},
|
||||||
"ruleSet": {"version": "entry-rules-7", "state": "active", "profileBinding": {"profileId": "main-stream", "width": 1920, "height": 1080}, "areas": [], "directionalLines": []},
|
"rule_set": {"version": "entry-rules-7", "state": "active", "profile_binding": {"profile_id": "main-stream", "width": 1920, "height": 1080}, "areas": [], "directional_lines": []},
|
||||||
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"schema": "yovision.source-config/v2",
|
"schema_version": "yovision.source-config/v2",
|
||||||
"configId": "school-east-entry-01",
|
"config_id": "school-east-entry-01",
|
||||||
"revision": 7,
|
"revision": 7,
|
||||||
"publishedAt": "2026-08-31T00:10:00Z",
|
"published_at": "2026-08-31T00:10:00Z",
|
||||||
"effectiveAt": "2026-08-31T00:15:00Z",
|
"effective_at": "2026-08-31T00:15:00Z",
|
||||||
"site": {"id": "site-east"},
|
"site": {"id": "site-east"},
|
||||||
"logicalDevice": {"id": "entry-camera-01"},
|
"logical_device": {"id": "entry-camera-01"},
|
||||||
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frameRate": 25},
|
"profile": {"id": "main-stream", "width": 1920, "height": 1080, "encoding": "H264", "frame_rate": 25},
|
||||||
"media": {"ref": "media:site-east/entry-01/main", "transport": "rtsp"},
|
"media": {"ref": "media:site-east/entry-01/main", "transport": "rtsp"},
|
||||||
"ruleSet": {"version": "entry-rules-7", "state": "active", "profileBinding": {"profileId": "main-stream", "width": 1920, "height": 1080}, "areas": [], "directionalLines": []},
|
"rule_set": {"version": "entry-rules-7", "state": "active", "profile_binding": {"profile_id": "main-stream", "width": 1920, "height": 1080}, "areas": [], "directional_lines": []},
|
||||||
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
"integrity": {"algorithm": "sha256", "value": "0000000000000000000000000000000000000000000000000000000000000000"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"schema": "yovision.source-config/v1",
|
"schema_version": "yovision.source-config/v1",
|
||||||
"configId": "school-east-entry-01",
|
"config_id": "school-east-entry-01",
|
||||||
"revision": 7,
|
"revision": 7,
|
||||||
"publishedAt": "2026-08-31T00:10:00Z",
|
"published_at": "2026-08-31T00:10:00Z",
|
||||||
"effectiveAt": "2026-08-31T00:15:00Z",
|
"effective_at": "2026-08-31T00:15:00Z",
|
||||||
"site": {
|
"site": {
|
||||||
"id": "site-east"
|
"id": "site-east"
|
||||||
},
|
},
|
||||||
"logicalDevice": {
|
"logical_device": {
|
||||||
"id": "entry-camera-01"
|
"id": "entry-camera-01"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
@@ -15,17 +15,17 @@
|
|||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080,
|
"height": 1080,
|
||||||
"encoding": "H264",
|
"encoding": "H264",
|
||||||
"frameRate": 25
|
"frame_rate": 25
|
||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"ref": "media:site-east/entry-01/main",
|
"ref": "media:site-east/entry-01/main",
|
||||||
"transport": "rtsp"
|
"transport": "rtsp"
|
||||||
},
|
},
|
||||||
"ruleSet": {
|
"rule_set": {
|
||||||
"version": "entry-rules-7",
|
"version": "entry-rules-7",
|
||||||
"state": "active",
|
"state": "active",
|
||||||
"profileBinding": {
|
"profile_binding": {
|
||||||
"profileId": "main-stream",
|
"profile_id": "main-stream",
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
},
|
},
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"directionalLines": [
|
"directional_lines": [
|
||||||
{
|
{
|
||||||
"id": "entry-line",
|
"id": "entry-line",
|
||||||
"version": 2,
|
"version": 2,
|
||||||
@@ -51,12 +51,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"start": {"x": 0.2, "y": 0.5},
|
"start": {"x": 0.2, "y": 0.5},
|
||||||
"end": {"x": 0.8, "y": 0.5},
|
"end": {"x": 0.8, "y": 0.5},
|
||||||
"triggerDirection": "left_to_right"
|
"trigger_direction": "left_to_right"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"integrity": {
|
"integrity": {
|
||||||
"algorithm": "sha256",
|
"algorithm": "sha256",
|
||||||
"value": "b35a4f345162b059d6912cca6340a6714826b7921662f4f28a91ef29c5476cb8"
|
"value": "3336fe595bf1401b1024ac0c95c31e1655228485465a4527900fcea2c713acfe"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"schema": "yovision.source-config/v1",
|
"schema_version": "yovision.source-config/v1",
|
||||||
"configId": "school-east-entry-01",
|
"config_id": "school-east-entry-01",
|
||||||
"revision": 8,
|
"revision": 8,
|
||||||
"publishedAt": "2026-08-31T01:00:00Z",
|
"published_at": "2026-08-31T01:00:00Z",
|
||||||
"effectiveAt": "2026-08-31T01:00:00Z",
|
"effective_at": "2026-08-31T01:00:00Z",
|
||||||
"site": {
|
"site": {
|
||||||
"id": "site-east"
|
"id": "site-east"
|
||||||
},
|
},
|
||||||
"logicalDevice": {
|
"logical_device": {
|
||||||
"id": "entry-camera-01"
|
"id": "entry-camera-01"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
@@ -15,25 +15,25 @@
|
|||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 720,
|
"height": 720,
|
||||||
"encoding": "H265",
|
"encoding": "H265",
|
||||||
"frameRate": 20
|
"frame_rate": 20
|
||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"ref": "media:site-east/entry-01/main-v2",
|
"ref": "media:site-east/entry-01/main-v2",
|
||||||
"transport": "rtsp"
|
"transport": "rtsp"
|
||||||
},
|
},
|
||||||
"ruleSet": {
|
"rule_set": {
|
||||||
"version": "entry-rules-8",
|
"version": "entry-rules-8",
|
||||||
"state": "recalibration_required",
|
"state": "recalibration_required",
|
||||||
"profileBinding": {
|
"profile_binding": {
|
||||||
"profileId": "main-stream-v2",
|
"profile_id": "main-stream-v2",
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 720
|
"height": 720
|
||||||
},
|
},
|
||||||
"areas": [],
|
"areas": [],
|
||||||
"directionalLines": []
|
"directional_lines": []
|
||||||
},
|
},
|
||||||
"integrity": {
|
"integrity": {
|
||||||
"algorithm": "sha256",
|
"algorithm": "sha256",
|
||||||
"value": "04408f00740558c70d9d1e3f9f5efbf03e6f22ccc0ba4ec6b6c8688df7b568df"
|
"value": "a53e6df8bab5c9a4e3f2dae2e82959939db09d34529af9ae65d66f322be833ba"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,32 +4,32 @@ mapper 必须创建新的契约 DTO,不得直接序列化 Sense GORM 实体,
|
|||||||
|
|
||||||
| 契约字段 | Sense 生产来源/规则 | Brain 消费目标/规则 |
|
| 契约字段 | Sense 生产来源/规则 | Brain 消费目标/规则 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `schema` | 常量 `yovision.source-config/v1` | 在任何映射前精确校验;未知主版本拒绝 |
|
| `schema_version` | 常量 `yovision.source-config/v1` | 在任何映射前精确校验;未知主版本拒绝 |
|
||||||
| `configId` | 新的稳定配置聚合 ID;不是数据库行 ID 语义 | 作为配置流逻辑 ID,不解释为 Brain 内部对象 ID |
|
| `config_id` | 新的稳定配置聚合 ID;不是数据库行 ID 语义 | 作为配置流逻辑 ID,不解释为 Brain 内部对象 ID |
|
||||||
| `revision` | 聚合配置变更时严格递增;不可复用 | 与 `configId` 共同做幂等、顺序和陈旧检查 |
|
| `revision` | 聚合配置变更时严格递增;不可复用 | 与 `config_id` 共同做幂等、顺序和陈旧检查 |
|
||||||
| `publishedAt` / `effectiveAt` | 发布时写 UTC RFC 3339;生效不得早于发布 | 完整校验后按生效时间原子切换 |
|
| `published_at` / `effective_at` | 发布时写 UTC RFC 3339;生效不得早于发布 | 完整校验后按生效时间原子切换 |
|
||||||
| `site.id` | 对外稳定站点引用;不得映射客户名或数据库主键语义 | 仅作租户隔离后的逻辑关联;v1 不提供用户身份 |
|
| `site.id` | 对外稳定站点引用;不得映射客户名或数据库主键语义 | 仅作租户隔离后的逻辑关联;v1 不提供用户身份 |
|
||||||
| `logicalDevice.id` | `area.Definition.DeviceID` / `media.Route.DeviceID` 经稳定外部 ID mapper | 映射到 `BrainInputConfig.logical_device_id` |
|
| `logical_device.id` | `area.Definition.DeviceID` / `media.Route.DeviceID` 经稳定外部 ID mapper | 映射到 `BrainInputConfig.logical_device_id` |
|
||||||
| `profile.id` | `area.Definition.ProfileToken` 与 `media.Route.ProfileToken` 经稳定 Profile ID mapper | 映射到 `BrainInputConfig.profile.profile_id` |
|
| `profile.id` | `area.Definition.ProfileToken` 与 `media.Route.ProfileToken` 经稳定 Profile ID mapper | 映射到 `BrainInputConfig.profile.profile_id` |
|
||||||
| `profile.width/height/encoding` | `area.Definition.ProfileWidth/ProfileHeight/ProfileEncoding`;必须与当前媒体 Profile 一致 | 映射到规则 `RuleSet` 的 Profile 绑定;不一致拒绝 |
|
| `profile.width/height/encoding` | `area.Definition.ProfileWidth/ProfileHeight/ProfileEncoding`;必须与当前媒体 Profile 一致 | 映射到规则 `RuleSet` 的 Profile 绑定;不一致拒绝 |
|
||||||
| `profile.frameRate` | Sense 已验证 Profile 的帧率快照 | 映射到 `BrainInputConfig.profile.fps` |
|
| `profile.frame_rate` | Sense 已验证 Profile 的帧率快照 | 映射到 `BrainInputConfig.profile.fps` |
|
||||||
| `media.ref` | 由 `media.Route.ID/Path` 生成 `media:<opaque-resource>`;禁止读取或拼入 `admissionProfile.StreamURI` 及凭据 | 交给后续 connector 解析;不得当作 RTSP URL 或本地路径 |
|
| `media.ref` | 由 `media.Route.ID/Path` 生成 `media:<opaque-resource>`;禁止读取或拼入 `admissionProfile.StreamURI` 及凭据 | 交给后续 connector 解析;不得当作 RTSP URL 或本地路径 |
|
||||||
| `media.transport` | 当前固定 `rtsp`,仅描述媒体传输类别 | 选择后续 connector/decode adapter;不含认证信息 |
|
| `media.transport` | 当前固定 `rtsp`,仅描述媒体传输类别 | 选择后续 connector/decode adapter;不含认证信息 |
|
||||||
| `ruleSet.version` | 由一组 `area.Version` 聚合成稳定规则集版本 | 映射到 Brain `RuleSet.version` |
|
| `rule_set.version` | 由一组 `area.Version` 聚合成稳定规则集版本 | 映射到 Brain `RuleSet.version` |
|
||||||
| `ruleSet.state` | `NeedsRecalibration=true` → `recalibration_required`;整体禁用 → `disabled`;否则 `active` | 只有 `active` 可构建并启用规则引擎 |
|
| `rule_set.state` | `NeedsRecalibration=true` → `recalibration_required`;整体禁用 → `disabled`;否则 `active` | 只有 `active` 可构建并启用规则引擎 |
|
||||||
| `ruleSet.profileBinding` | 与本快照 `profile.id/width/height` 同源复制并交叉校验 | 必须精确等于 `profile`;之后才接受归一化几何 |
|
| `rule_set.profile_binding` | 与本快照 `profile.id/width/height` 同源复制并交叉校验 | 必须精确等于 `profile`;之后才接受归一化几何 |
|
||||||
| `ruleSet.areas[].id/version` | `area.Version.DefinitionID/Version` 经稳定规则 ID mapper | 映射到 `AreaRule.rule_id`;version 用于可追溯性 |
|
| `rule_set.areas[].id/version` | `area.Version.DefinitionID/Version` 经稳定规则 ID mapper | 映射到 `AreaRule.rule_id`;version 用于可追溯性 |
|
||||||
| `ruleSet.areas[].kind` | Sense `polygon` 映射为 `danger_area` | 只映射到 Brain 危险区域规则,不透传 Sense 枚举 |
|
| `rule_set.areas[].kind` | Sense `polygon` 映射为 `danger_area` | 只映射到 Brain 危险区域规则,不透传 Sense 枚举 |
|
||||||
| `ruleSet.areas[].points` | `area.Version.GeometryJSON` 中 `{x,y}`;保持 0–1 | 映射到 Brain `Point(x,y)`;至少三点且非退化 |
|
| `rule_set.areas[].points` | `area.Version.GeometryJSON` 中 `{x,y}`;保持 0–1 | 映射到 Brain `Point(x,y)`;至少三点且非退化 |
|
||||||
| `ruleSet.directionalLines[].id/version` | `area.Version.DefinitionID/Version` 经稳定规则 ID mapper | 映射到 `DirectionalLineRule.rule_id` |
|
| `rule_set.directional_lines[].id/version` | `area.Version.DefinitionID/Version` 经稳定规则 ID mapper | 映射到 `DirectionalLineRule.rule_id` |
|
||||||
| `ruleSet.directionalLines[].start/end` | `direction_line` 几何的两个归一化点 | 映射到 Brain `Point`;相同点拒绝 |
|
| `rule_set.directional_lines[].start/end` | `direction_line` 几何的两个归一化点 | 映射到 Brain `Point`;相同点拒绝 |
|
||||||
| `ruleSet.directionalLines[].triggerDirection` | Sense `forward/reverse` 必须由 mapper 根据已确认的起终点方向转换为 `left_to_right/right_to_left` | 映射到 `DirectionalLineRule.trigger_direction`;不得直接猜测枚举 |
|
| `rule_set.directional_lines[].trigger_direction` | Sense `forward/reverse` 必须由 mapper 根据已确认的起终点方向转换为 `left_to_right/right_to_left` | 映射到 `DirectionalLineRule.trigger_direction`;不得直接猜测枚举 |
|
||||||
| `integrity` | 对移除 `integrity` 的 JCS 快照计算 SHA-256 | 映射前重算并常量时间比较;失败保留上一有效 revision |
|
| `integrity` | 对移除 `integrity` 的 JCS 快照计算 SHA-256 | 映射前重算并常量时间比较;失败保留上一有效 revision |
|
||||||
|
|
||||||
## 测试责任
|
## 测试责任
|
||||||
|
|
||||||
- Sense 生产者契约测试:从设备、媒体 Route、Profile 与区域版本 fixture 生成快照;断言字段映射、revision 递增、Profile 变化触发新 revision/待重校准、无秘密媒体引用、Schema/语义/摘要通过。
|
- Sense 生产者契约测试:从设备、媒体 Route、Profile 与区域版本 fixture 生成快照;断言字段映射、revision 递增、Profile 变化触发新 revision/待重校准、无秘密媒体引用、Schema/语义/摘要通过。
|
||||||
- Brain 消费者契约测试:加载本目录有效与无效样例;断言版本拒绝、幂等/陈旧处理、Profile 绑定、坐标、规则 ID、状态门禁和摘要;再映射为 Brain 内部配置,证明共享 `schema` 不等于 `brain.internal.input/v1`。
|
- Brain 消费者契约测试:加载本目录有效与无效样例;断言版本拒绝、幂等/陈旧处理、Profile 绑定、坐标、规则 ID、状态门禁和摘要;再映射为 Brain 内部配置,证明共享 `schema_version` 不等于 `brain.internal.input/v1`。
|
||||||
- 协调契约测试(本工单):校验所有样例、秘密字段/URL/本地路径拒绝、跨字段语义和摘要。产品 adapter 测试在后续 connector 工单实施。
|
- 协调契约测试(本工单):校验所有样例、秘密字段/URL/本地路径拒绝、跨字段语义和摘要。产品 adapter 测试在后续 connector 工单实施。
|
||||||
|
|
||||||
Sense 与 Brain 各自可增加内部字段,但不得将数据库主键、用户表、JWT、Cookie、摄像头凭据、客户内部路径或内部模型直接扩展进本契约。
|
Sense 与 Brain 各自可增加内部字段,但不得将数据库主键、用户表、JWT、Cookie、摄像头凭据、客户内部路径或内部模型直接扩展进本契约。
|
||||||
|
|||||||
@@ -6,34 +6,34 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"schema",
|
"schema_version",
|
||||||
"configId",
|
"config_id",
|
||||||
"revision",
|
"revision",
|
||||||
"publishedAt",
|
"published_at",
|
||||||
"effectiveAt",
|
"effective_at",
|
||||||
"site",
|
"site",
|
||||||
"logicalDevice",
|
"logical_device",
|
||||||
"profile",
|
"profile",
|
||||||
"media",
|
"media",
|
||||||
"ruleSet",
|
"rule_set",
|
||||||
"integrity"
|
"integrity"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"schema": {
|
"schema_version": {
|
||||||
"const": "yovision.source-config/v1"
|
"const": "yovision.source-config/v1"
|
||||||
},
|
},
|
||||||
"configId": {
|
"config_id": {
|
||||||
"$ref": "#/$defs/stableId"
|
"$ref": "#/$defs/stable_id"
|
||||||
},
|
},
|
||||||
"revision": {
|
"revision": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 1
|
"minimum": 1
|
||||||
},
|
},
|
||||||
"publishedAt": {
|
"published_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"effectiveAt": {
|
"effective_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
@@ -43,17 +43,17 @@
|
|||||||
"required": ["id"],
|
"required": ["id"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"$ref": "#/$defs/stableId"
|
"$ref": "#/$defs/stable_id"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"logicalDevice": {
|
"logical_device": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["id"],
|
"required": ["id"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"$ref": "#/$defs/stableId"
|
"$ref": "#/$defs/stable_id"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -75,37 +75,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ruleSet": {
|
"rule_set": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"state",
|
"state",
|
||||||
"profileBinding",
|
"profile_binding",
|
||||||
"areas",
|
"areas",
|
||||||
"directionalLines"
|
"directional_lines"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"version": {
|
"version": {
|
||||||
"$ref": "#/$defs/stableId"
|
"$ref": "#/$defs/stable_id"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"enum": ["active", "disabled", "recalibration_required"]
|
"enum": ["active", "disabled", "recalibration_required"]
|
||||||
},
|
},
|
||||||
"profileBinding": {
|
"profile_binding": {
|
||||||
"$ref": "#/$defs/profileBinding"
|
"$ref": "#/$defs/profile_binding"
|
||||||
},
|
},
|
||||||
"areas": {
|
"areas": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/areaRule"
|
"$ref": "#/$defs/area_rule"
|
||||||
},
|
},
|
||||||
"maxItems": 1024
|
"maxItems": 1024
|
||||||
},
|
},
|
||||||
"directionalLines": {
|
"directional_lines": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/directionalLineRule"
|
"$ref": "#/$defs/directional_line_rule"
|
||||||
},
|
},
|
||||||
"maxItems": 1024
|
"maxItems": 1024
|
||||||
}
|
}
|
||||||
@@ -137,55 +137,55 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"stableId": {
|
"stable_id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"maxLength": 128,
|
"maxLength": 128,
|
||||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._~-]*$"
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._~-]*$"
|
||||||
},
|
},
|
||||||
"positiveInteger": {
|
"positive_integer": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 1
|
"minimum": 1
|
||||||
},
|
},
|
||||||
"positiveNumber": {
|
"positive_number": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"exclusiveMinimum": 0
|
"exclusiveMinimum": 0
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["id", "width", "height", "encoding", "frameRate"],
|
"required": ["id", "width", "height", "encoding", "frame_rate"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"$ref": "#/$defs/stableId"
|
"$ref": "#/$defs/stable_id"
|
||||||
},
|
},
|
||||||
"width": {
|
"width": {
|
||||||
"$ref": "#/$defs/positiveInteger"
|
"$ref": "#/$defs/positive_integer"
|
||||||
},
|
},
|
||||||
"height": {
|
"height": {
|
||||||
"$ref": "#/$defs/positiveInteger"
|
"$ref": "#/$defs/positive_integer"
|
||||||
},
|
},
|
||||||
"encoding": {
|
"encoding": {
|
||||||
"enum": ["H264", "H265", "MJPEG"]
|
"enum": ["H264", "H265", "MJPEG"]
|
||||||
},
|
},
|
||||||
"frameRate": {
|
"frame_rate": {
|
||||||
"$ref": "#/$defs/positiveNumber"
|
"$ref": "#/$defs/positive_number"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profileBinding": {
|
"profile_binding": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["profileId", "width", "height"],
|
"required": ["profile_id", "width", "height"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"profileId": {
|
"profile_id": {
|
||||||
"$ref": "#/$defs/stableId"
|
"$ref": "#/$defs/stable_id"
|
||||||
},
|
},
|
||||||
"width": {
|
"width": {
|
||||||
"$ref": "#/$defs/positiveInteger"
|
"$ref": "#/$defs/positive_integer"
|
||||||
},
|
},
|
||||||
"height": {
|
"height": {
|
||||||
"$ref": "#/$defs/positiveInteger"
|
"$ref": "#/$defs/positive_integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -206,16 +206,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"areaRule": {
|
"area_rule": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["id", "version", "kind", "enabled", "points"],
|
"required": ["id", "version", "kind", "enabled", "points"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"$ref": "#/$defs/stableId"
|
"$ref": "#/$defs/stable_id"
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"$ref": "#/$defs/positiveInteger"
|
"$ref": "#/$defs/positive_integer"
|
||||||
},
|
},
|
||||||
"kind": {
|
"kind": {
|
||||||
"const": "danger_area"
|
"const": "danger_area"
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"directionalLineRule": {
|
"directional_line_rule": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
@@ -243,14 +243,14 @@
|
|||||||
"enabled",
|
"enabled",
|
||||||
"start",
|
"start",
|
||||||
"end",
|
"end",
|
||||||
"triggerDirection"
|
"trigger_direction"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"$ref": "#/$defs/stableId"
|
"$ref": "#/$defs/stable_id"
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"$ref": "#/$defs/positiveInteger"
|
"$ref": "#/$defs/positive_integer"
|
||||||
},
|
},
|
||||||
"kind": {
|
"kind": {
|
||||||
"const": "directional_line"
|
"const": "directional_line"
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
"end": {
|
"end": {
|
||||||
"$ref": "#/$defs/point"
|
"$ref": "#/$defs/point"
|
||||||
},
|
},
|
||||||
"triggerDirection": {
|
"trigger_direction": {
|
||||||
"enum": ["left_to_right", "right_to_left"]
|
"enum": ["left_to_right", "right_to_left"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def polygon_area(points: list[dict[str, float]]) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def validate_payload(payload: dict[str, Any]) -> None:
|
def validate_payload(payload: dict[str, Any]) -> None:
|
||||||
if payload.get("schema") != "yovision.source-config/v1":
|
if payload.get("schema_version") != "yovision.source-config/v1":
|
||||||
raise ValueError("unknown schema major version")
|
raise ValueError("unknown schema major version")
|
||||||
|
|
||||||
reject_secrets(payload)
|
reject_secrets(payload)
|
||||||
@@ -86,21 +86,21 @@ def validate_payload(payload: dict[str, Any]) -> None:
|
|||||||
raise ValueError(f"schema validation failed at {location}: {first.message}")
|
raise ValueError(f"schema validation failed at {location}: {first.message}")
|
||||||
|
|
||||||
profile = payload["profile"]
|
profile = payload["profile"]
|
||||||
binding = payload["ruleSet"]["profileBinding"]
|
binding = payload["rule_set"]["profile_binding"]
|
||||||
if (binding["profileId"], binding["width"], binding["height"]) != (
|
if (binding["profile_id"], binding["width"], binding["height"]) != (
|
||||||
profile["id"],
|
profile["id"],
|
||||||
profile["width"],
|
profile["width"],
|
||||||
profile["height"],
|
profile["height"],
|
||||||
):
|
):
|
||||||
raise ValueError("profile binding does not match the media profile")
|
raise ValueError("profile binding does not match the media profile")
|
||||||
|
|
||||||
published_at = datetime.fromisoformat(payload["publishedAt"].replace("Z", "+00:00"))
|
published_at = datetime.fromisoformat(payload["published_at"].replace("Z", "+00:00"))
|
||||||
effective_at = datetime.fromisoformat(payload["effectiveAt"].replace("Z", "+00:00"))
|
effective_at = datetime.fromisoformat(payload["effective_at"].replace("Z", "+00:00"))
|
||||||
if effective_at < published_at:
|
if effective_at < published_at:
|
||||||
raise ValueError("effectiveAt precedes publishedAt")
|
raise ValueError("effective_at precedes published_at")
|
||||||
|
|
||||||
rule_set = payload["ruleSet"]
|
rule_set = payload["rule_set"]
|
||||||
rules = [*rule_set["areas"], *rule_set["directionalLines"]]
|
rules = [*rule_set["areas"], *rule_set["directional_lines"]]
|
||||||
identifiers = [rule["id"] for rule in rules]
|
identifiers = [rule["id"] for rule in rules]
|
||||||
if len(identifiers) != len(set(identifiers)):
|
if len(identifiers) != len(set(identifiers)):
|
||||||
raise ValueError("rule ids must be unique across the rule set")
|
raise ValueError("rule ids must be unique across the rule set")
|
||||||
@@ -110,7 +110,7 @@ def validate_payload(payload: dict[str, Any]) -> None:
|
|||||||
for area in rule_set["areas"]:
|
for area in rule_set["areas"]:
|
||||||
if polygon_area(area["points"]) <= 1e-12:
|
if polygon_area(area["points"]) <= 1e-12:
|
||||||
raise ValueError(f"area {area['id']} is a degenerate polygon")
|
raise ValueError(f"area {area['id']} is a degenerate polygon")
|
||||||
for line in rule_set["directionalLines"]:
|
for line in rule_set["directional_lines"]:
|
||||||
if line["start"] == line["end"]:
|
if line["start"] == line["end"]:
|
||||||
raise ValueError(f"directional line {line['id']} has identical endpoints")
|
raise ValueError(f"directional line {line['id']} has identical endpoints")
|
||||||
|
|
||||||
@@ -121,8 +121,8 @@ def validate_payload(payload: dict[str, Any]) -> None:
|
|||||||
def validate_transition(previous: dict[str, Any], current: dict[str, Any]) -> None:
|
def validate_transition(previous: dict[str, Any], current: dict[str, Any]) -> None:
|
||||||
validate_payload(previous)
|
validate_payload(previous)
|
||||||
validate_payload(current)
|
validate_payload(current)
|
||||||
if previous["configId"] != current["configId"]:
|
if previous["config_id"] != current["config_id"]:
|
||||||
raise ValueError("configId cannot change within one revision stream")
|
raise ValueError("config_id cannot change within one revision stream")
|
||||||
if current["revision"] <= previous["revision"]:
|
if current["revision"] <= previous["revision"]:
|
||||||
raise ValueError("revision must increase strictly")
|
raise ValueError("revision must increase strictly")
|
||||||
|
|
||||||
@@ -134,16 +134,16 @@ def validate_transition(previous: dict[str, Any], current: dict[str, Any]) -> No
|
|||||||
)
|
)
|
||||||
previous_rule_versions = sorted(
|
previous_rule_versions = sorted(
|
||||||
(rule["id"], rule["version"])
|
(rule["id"], rule["version"])
|
||||||
for rule in [*previous["ruleSet"]["areas"], *previous["ruleSet"]["directionalLines"]]
|
for rule in [*previous["rule_set"]["areas"], *previous["rule_set"]["directional_lines"]]
|
||||||
)
|
)
|
||||||
current_rule_versions = sorted(
|
current_rule_versions = sorted(
|
||||||
(rule["id"], rule["version"])
|
(rule["id"], rule["version"])
|
||||||
for rule in [*current["ruleSet"]["areas"], *current["ruleSet"]["directionalLines"]]
|
for rule in [*current["rule_set"]["areas"], *current["rule_set"]["directional_lines"]]
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
profile_changed
|
profile_changed
|
||||||
and previous_rule_versions == current_rule_versions
|
and previous_rule_versions == current_rule_versions
|
||||||
and current["ruleSet"]["state"] != "recalibration_required"
|
and current["rule_set"]["state"] != "recalibration_required"
|
||||||
):
|
):
|
||||||
raise ValueError("profile changed without rule recalibration state or new rule versions")
|
raise ValueError("profile changed without rule recalibration state or new rule versions")
|
||||||
|
|
||||||
@@ -186,13 +186,13 @@ class SourceConfigV1ContractTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_profile_revision_and_recalibration_semantics_are_safe(self) -> None:
|
def test_profile_revision_and_recalibration_semantics_are_safe(self) -> None:
|
||||||
payload = load_json(VALID_ROOT / "active.json")
|
payload = load_json(VALID_ROOT / "active.json")
|
||||||
payload["ruleSet"]["profileBinding"]["width"] = 1280
|
payload["rule_set"]["profile_binding"]["width"] = 1280
|
||||||
set_integrity(payload)
|
set_integrity(payload)
|
||||||
with self.assertRaisesRegex(ValueError, "profile binding"):
|
with self.assertRaisesRegex(ValueError, "profile binding"):
|
||||||
validate_payload(payload)
|
validate_payload(payload)
|
||||||
|
|
||||||
payload = load_json(VALID_ROOT / "active.json")
|
payload = load_json(VALID_ROOT / "active.json")
|
||||||
payload["ruleSet"]["state"] = "recalibration_required"
|
payload["rule_set"]["state"] = "recalibration_required"
|
||||||
set_integrity(payload)
|
set_integrity(payload)
|
||||||
with self.assertRaisesRegex(ValueError, "must not remain enabled"):
|
with self.assertRaisesRegex(ValueError, "must not remain enabled"):
|
||||||
validate_payload(payload)
|
validate_payload(payload)
|
||||||
@@ -207,8 +207,8 @@ class SourceConfigV1ContractTests(unittest.TestCase):
|
|||||||
|
|
||||||
current["revision"] += 1
|
current["revision"] += 1
|
||||||
current["profile"].update({"id": "main-stream-v2", "width": 1280, "height": 720})
|
current["profile"].update({"id": "main-stream-v2", "width": 1280, "height": 720})
|
||||||
current["ruleSet"]["profileBinding"].update(
|
current["rule_set"]["profile_binding"].update(
|
||||||
{"profileId": "main-stream-v2", "width": 1280, "height": 720}
|
{"profile_id": "main-stream-v2", "width": 1280, "height": 720}
|
||||||
)
|
)
|
||||||
set_integrity(current)
|
set_integrity(current)
|
||||||
with self.assertRaisesRegex(ValueError, "without rule recalibration"):
|
with self.assertRaisesRegex(ValueError, "without rule recalibration"):
|
||||||
@@ -218,7 +218,7 @@ class SourceConfigV1ContractTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_rule_geometry_and_global_ids_are_semantically_validated(self) -> None:
|
def test_rule_geometry_and_global_ids_are_semantically_validated(self) -> None:
|
||||||
payload = load_json(VALID_ROOT / "active.json")
|
payload = load_json(VALID_ROOT / "active.json")
|
||||||
payload["ruleSet"]["areas"][0]["points"] = [
|
payload["rule_set"]["areas"][0]["points"] = [
|
||||||
{"x": 0, "y": 0},
|
{"x": 0, "y": 0},
|
||||||
{"x": 0.5, "y": 0.5},
|
{"x": 0.5, "y": 0.5},
|
||||||
{"x": 1, "y": 1},
|
{"x": 1, "y": 1},
|
||||||
@@ -228,14 +228,14 @@ class SourceConfigV1ContractTests(unittest.TestCase):
|
|||||||
validate_payload(payload)
|
validate_payload(payload)
|
||||||
|
|
||||||
payload = load_json(VALID_ROOT / "active.json")
|
payload = load_json(VALID_ROOT / "active.json")
|
||||||
payload["ruleSet"]["directionalLines"][0]["id"] = payload["ruleSet"]["areas"][0]["id"]
|
payload["rule_set"]["directional_lines"][0]["id"] = payload["rule_set"]["areas"][0]["id"]
|
||||||
set_integrity(payload)
|
set_integrity(payload)
|
||||||
with self.assertRaisesRegex(ValueError, "ids must be unique"):
|
with self.assertRaisesRegex(ValueError, "ids must be unique"):
|
||||||
validate_payload(payload)
|
validate_payload(payload)
|
||||||
|
|
||||||
def test_effective_time_cannot_precede_publication(self) -> None:
|
def test_effective_time_cannot_precede_publication(self) -> None:
|
||||||
payload = load_json(VALID_ROOT / "active.json")
|
payload = load_json(VALID_ROOT / "active.json")
|
||||||
payload["effectiveAt"] = "2026-08-30T23:59:59Z"
|
payload["effective_at"] = "2026-08-30T23:59:59Z"
|
||||||
set_integrity(payload)
|
set_integrity(payload)
|
||||||
with self.assertRaisesRegex(ValueError, "precedes"):
|
with self.assertRaisesRegex(ValueError, "precedes"):
|
||||||
validate_payload(payload)
|
validate_payload(payload)
|
||||||
|
|||||||
Reference in New Issue
Block a user