feat: standardize algorithm measurement results
This commit is contained in:
136
alg/00_算法交付与输出规范/validate_result.py
Executable file
136
alg/00_算法交付与输出规范/validate_result.py
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""算法测量结果契约 1.0 的零第三方依赖校验器。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
METRIC_CODE_RE = re.compile(r"^[a-z0-9]+(?:[._-][a-z0-9]+)*$")
|
||||
TASK_NO_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
||||
TASK_NAME_PREFIX_RE = re.compile(r"^任务\s*\d+")
|
||||
FINAL_STATUSES = {"pass", "warning", "fail"}
|
||||
|
||||
|
||||
def is_number(value: Any) -> bool:
|
||||
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
|
||||
|
||||
|
||||
def validate(payload: Any) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if not isinstance(payload, dict):
|
||||
return ["根节点必须是 JSON 对象"]
|
||||
|
||||
if not isinstance(payload.get("schemaVersion"), int) or payload["schemaVersion"] < 1:
|
||||
errors.append("schemaVersion 必须是大于等于 1 的整数")
|
||||
if payload.get("measurementContractVersion") != "1.0":
|
||||
errors.append('measurementContractVersion 必须为 "1.0"')
|
||||
if payload.get("status") not in {"completed", "error"}:
|
||||
errors.append("status 必须为 completed 或 error")
|
||||
|
||||
measurements = payload.get("measurements")
|
||||
if payload.get("status") == "completed" and not isinstance(measurements, list):
|
||||
errors.append("completed 结果必须包含 measurements 数组")
|
||||
measurements = []
|
||||
elif measurements is None:
|
||||
measurements = []
|
||||
elif not isinstance(measurements, list):
|
||||
errors.append("measurements 必须是数组")
|
||||
measurements = []
|
||||
|
||||
unique_keys: set[tuple[str, str, str]] = set()
|
||||
for index, item in enumerate(measurements):
|
||||
path = f"measurements[{index}]"
|
||||
if not isinstance(item, dict):
|
||||
errors.append(f"{path} 必须是对象")
|
||||
continue
|
||||
|
||||
task_no = item.get("taskNo")
|
||||
metric_code = item.get("metricCode")
|
||||
name = item.get("name")
|
||||
position = item.get("position", "")
|
||||
value = item.get("value", ...)
|
||||
status = item.get("status")
|
||||
|
||||
if not isinstance(task_no, str) or not TASK_NO_RE.fullmatch(task_no):
|
||||
errors.append(f"{path}.taskNo 必须是非空规范字符串,数字任务号也要加引号")
|
||||
if not isinstance(metric_code, str) or not METRIC_CODE_RE.fullmatch(metric_code):
|
||||
errors.append(f"{path}.metricCode 格式无效")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
errors.append(f"{path}.name 必须是非空字符串")
|
||||
elif TASK_NAME_PREFIX_RE.search(name):
|
||||
errors.append(f"{path}.name 不得包含任务号前缀,请使用 taskNo 字段")
|
||||
if position and not isinstance(position, str):
|
||||
errors.append(f"{path}.position 必须是字符串")
|
||||
if value is ... or (value is not None and not is_number(value)):
|
||||
errors.append(f"{path}.value 必须是有限数值或 null")
|
||||
if status not in FINAL_STATUSES:
|
||||
errors.append(f"{path}.status 必须为 pass、warning 或 fail,正式结果不得使用 info")
|
||||
if value is None and status == "pass":
|
||||
errors.append(f"{path} 的 value 为 null 时不能判定为 pass")
|
||||
if "baseline" in item and (not is_number(item["baseline"]) or item["baseline"] <= 0):
|
||||
errors.append(f"{path}.baseline 必须是正数")
|
||||
if "tolerance" in item and (not is_number(item["tolerance"]) or item["tolerance"] < 0):
|
||||
errors.append(f"{path}.tolerance 必须是非负有限数值")
|
||||
|
||||
if isinstance(task_no, str) and isinstance(metric_code, str) and isinstance(position, str):
|
||||
key = (task_no, metric_code, position)
|
||||
if key in unique_keys:
|
||||
errors.append(f"{path} 与其他正式测量项重复:taskNo + metricCode + position 必须唯一")
|
||||
unique_keys.add(key)
|
||||
|
||||
extra = payload.get("extra", {})
|
||||
if extra is not None and not isinstance(extra, dict):
|
||||
errors.append("extra 必须是对象")
|
||||
extra = {}
|
||||
intermediate = extra.get("intermediateMetrics", []) if isinstance(extra, dict) else []
|
||||
if not isinstance(intermediate, list):
|
||||
errors.append("extra.intermediateMetrics 必须是数组")
|
||||
intermediate = []
|
||||
for index, item in enumerate(intermediate):
|
||||
path = f"extra.intermediateMetrics[{index}]"
|
||||
if not isinstance(item, dict):
|
||||
errors.append(f"{path} 必须是对象")
|
||||
continue
|
||||
if any(field in item for field in ("taskNo", "status", "tolerance")):
|
||||
errors.append(f"{path} 不得包含 taskNo、status 或 tolerance")
|
||||
code = item.get("metricCode")
|
||||
if not isinstance(code, str) or not METRIC_CODE_RE.fullmatch(code):
|
||||
errors.append(f"{path}.metricCode 格式无效")
|
||||
if not isinstance(item.get("name"), str) or not item["name"].strip():
|
||||
errors.append(f"{path}.name 必须是非空字符串")
|
||||
value = item.get("value", ...)
|
||||
if value is ... or (value is not None and not is_number(value)):
|
||||
errors.append(f"{path}.value 必须是有限数值或 null")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
print(f"用法: {Path(sys.argv[0]).name} /path/to/result.json", file=sys.stderr)
|
||||
return 2
|
||||
path = Path(sys.argv[1])
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"), parse_constant=lambda value: (_ for _ in ()).throw(ValueError(f"非法数值 {value}")))
|
||||
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
||||
print(f"校验失败: 无法读取标准 JSON:{exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
errors = validate(payload)
|
||||
if errors:
|
||||
print(f"校验失败,共 {len(errors)} 个问题:", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f"- {error}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"校验通过:{path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user