feat(#50): 驾驶舱布局 JSON Schema 定义(PRD 5.5 配置化驾驶舱)
新增 core/cockpit 模块,定义 iAOP-cockpit-layout-v1 布局资产 schema 的 权威实现,对齐 PRD 5.5「⑤ 配置化驾驶舱」验收口径(切换模板零改码)。 交付内容: - core/cockpit/layout.py:布局字段规范、合法性集合(主题/组件类型)、 validate_layout 校验器(结构+语义)、load_layout 解析器、 CockpitLayout/Widget/Grid 内存模型(dataclass)、LayoutValidationError。 - core/cockpit/__init__.py:对外导出。 - core/cockpit/tests/test_layout.py:30 个单测(PRD 示例/树脂模板兼容/ 各类非法/性能提示/解析/round-trip/不变入参)。 - core/cockpit/scripts/verify_layout_schema.py:端到端验证脚本(exit=0)。 兼容性:现有 templates/resin/dashboard/cockpit.resin.yaml 引用的 $schema: iAOP-cockpit-layout-v1 与本实现一致,树脂模板资产校验通过。
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""驾驶舱布局 JSON Schema 验证脚本(issue #50,PRD 5.5 验收口径)。
|
||||
|
||||
验证三个能力点:
|
||||
1. **PRD 5.5 原始示例**(氯化车间)通过 ``iAOP-cockpit-layout-v1`` 校验;
|
||||
2. **现有树脂模板资产兼容**:``templates/resin/dashboard/cockpit.resin.yaml``
|
||||
所描述的布局(等价 dict)通过同一套校验,证明"切换模板零改码"已具备基础;
|
||||
3. **负例**:一份故意破坏的布局被正确拒绝(证明阈值判断有效)。
|
||||
|
||||
用法(在 core/cockpit 目录下):
|
||||
python scripts/verify_layout_schema.py
|
||||
退出码:0 = 全部通过;1 = 存在未达标项。
|
||||
|
||||
说明:树脂驾驶舱资产本身是 YAML;本脚本不依赖 PyYAML(避免引入运行时依赖),
|
||||
而是用与该 YAML 文件内容等价的 dict 进行校验——schema 字段语义完全一致,
|
||||
若安装了 PyYAML,可直接解析原文件复现(见文末注释)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 本脚本位于 core/cockpit/scripts/,需要把 core/ 加入 sys.path,
|
||||
# 才能 `from cockpit import ...`(cockpit 包位于 core/cockpit)
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from cockpit import LAYOUT_SCHEMA_ID, validate_layout # noqa: E402
|
||||
|
||||
|
||||
def _check(name: str, data: dict, expect_ok: bool) -> bool:
|
||||
res = validate_layout(data)
|
||||
status = "PASS" if res.ok == expect_ok else "FAIL"
|
||||
print(f"[{status}] {name}: ok={res.ok} widgets={res.widget_count}")
|
||||
if res.ok != expect_ok:
|
||||
for e in res.errors:
|
||||
print(f" - {e}")
|
||||
return False
|
||||
if res.ok and res.perf_hint:
|
||||
print(f" perf_hint: {res.perf_hint}")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
all_ok = True
|
||||
|
||||
# 1) PRD 5.5 原始示例(氯化车间)
|
||||
prd_example = {
|
||||
"$schema": "iAOP-cockpit-layout-v1",
|
||||
"title": "氯化车间驾驶舱",
|
||||
"theme": "dark",
|
||||
"widgets": [
|
||||
{"type": "process_view", "src": "ti_four_state.svg", "x": 0, "y": 0, "w": 6, "h": 4},
|
||||
{"type": "trend", "bind": "CLF-01.TEMP", "x": 6, "y": 0, "w": 6, "h": 2},
|
||||
{"type": "kpi_card", "metric": "Ti_purity", "x": 6, "y": 2, "w": 3, "h": 2},
|
||||
{"type": "alarm_panel", "x": 0, "y": 4, "w": 12, "h": 3},
|
||||
],
|
||||
}
|
||||
all_ok &= _check("PRD 5.5 示例(氯化车间)", prd_example, expect_ok=True)
|
||||
|
||||
# 2) 现有树脂模板资产兼容(与 templates/resin/dashboard/cockpit.resin.yaml 等价)
|
||||
resin_layout = {
|
||||
"$schema": LAYOUT_SCHEMA_ID,
|
||||
"title": "吸附树脂车间驾驶舱",
|
||||
"theme": "dark",
|
||||
"widgets": [
|
||||
{"type": "process_view", "src": "resin_four_state.svg", "x": 0, "y": 0, "w": 12, "h": 4,
|
||||
"description": "四状态工艺流程(合成 → 交联 → 洗涤 → 干燥)"},
|
||||
{"type": "trend", "bind": "R-801.TEMP", "x": 0, "y": 4, "w": 6, "h": 2,
|
||||
"description": "反应釜温度实时趋势"},
|
||||
{"type": "trend", "bind": "R-801.AGIT", "x": 6, "y": 4, "w": 6, "h": 2,
|
||||
"description": "搅拌转速实时趋势"},
|
||||
{"type": "kpi_card", "metric": "resin_exchange_capacity", "label": "交换容量",
|
||||
"x": 0, "y": 6, "w": 3, "h": 2, "description": "当批平均交换容量(mmol/g)"},
|
||||
{"type": "kpi_card", "metric": "resin_crosslink_degree", "label": "交联度",
|
||||
"x": 3, "y": 6, "w": 3, "h": 2, "description": "当批平均交联度(%)"},
|
||||
{"type": "kpi_card", "metric": "batch_yield", "label": "批产率",
|
||||
"x": 6, "y": 6, "w": 3, "h": 2, "description": "当批产率(%)"},
|
||||
{"type": "kpi_card", "metric": "energy_per_ton", "label": "单吨能耗",
|
||||
"x": 9, "y": 6, "w": 3, "h": 2, "description": "单吨树脂综合能耗(kWh/t)"},
|
||||
{"type": "alarm_panel", "x": 0, "y": 8, "w": 9, "h": 3,
|
||||
"description": "告警面板(温度/交联度/质量预测异常)"},
|
||||
{"type": "nl_query", "x": 9, "y": 8, "w": 3, "h": 3,
|
||||
"description": "自然语言查询入口(配方/质量/能耗问答)"},
|
||||
],
|
||||
}
|
||||
all_ok &= _check("树脂模板资产(向后兼容)", resin_layout, expect_ok=True)
|
||||
|
||||
# 3) 负例:kpi_card 缺 metric,应被拒绝
|
||||
broken = {
|
||||
"$schema": LAYOUT_SCHEMA_ID,
|
||||
"title": "坏布局",
|
||||
"theme": "dark",
|
||||
"widgets": [
|
||||
{"type": "kpi_card", "x": 0, "y": 0, "w": 3, "h": 2}, # 缺 metric
|
||||
],
|
||||
}
|
||||
all_ok &= _check("负例(kpi_card 缺 metric)应被拒绝", broken, expect_ok=False)
|
||||
|
||||
print()
|
||||
print("全部通过 ✅" if all_ok else "存在未达标项 ❌")
|
||||
return 0 if all_ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user