Files

185 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Validate a machine-readable BSP GPIO implementation plan."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
DIRECTIONS = {"input", "output", "peripheral", "none"}
ACTIVE_LEVELS = {"high", "low", "paired", "unknown", None}
DECISIONS = {"confirmed", "inherited", "no-gpio", "provisional", "blocked"}
IMPLEMENTABLE = {"confirmed", "inherited", "no-gpio"}
REQUIRED_SIGNAL_FIELDS = {
"function",
"pad",
"mux",
"gpio",
"direction",
"active_level",
"boot_value",
"runtime_owner",
"dts_consumers",
"source_evidence",
"decision",
"note",
}
def validate_plan(payload: object, allow_unconfirmed: bool = False) -> dict[str, object]:
errors: list[str] = []
warnings: list[str] = []
if not isinstance(payload, dict):
return {"passed": False, "errors": ["顶层必须是 JSON 对象"], "warnings": []}
if payload.get("schema_version") != 1:
errors.append("schema_version 必须为 1")
if not isinstance(payload.get("project"), str) or not payload.get("project", "").strip():
errors.append("project 必须是非空字符串")
if payload.get("validation_mode") not in {"emxres-config", "full-dts-audit"}:
errors.append("validation_mode 必须为 emxres-config 或 full-dts-audit")
sources = payload.get("sources")
if not isinstance(sources, list) or not sources or not all(
isinstance(item, str) and item.strip() for item in sources
):
errors.append("sources 必须是非空字符串数组")
confirmation = payload.get("confirmation")
confirmed = isinstance(confirmation, dict) and confirmation.get("confirmed") is True
if not confirmed and not allow_unconfirmed:
errors.append("confirmation.confirmed 必须为 true")
if confirmed:
if not isinstance(confirmation.get("confirmed_at"), str) or not confirmation.get(
"confirmed_at", ""
).strip():
errors.append("确认后必须填写 confirmation.confirmed_at")
if not isinstance(confirmation.get("summary"), str) or not confirmation.get(
"summary", ""
).strip():
errors.append("确认后必须填写 confirmation.summary")
signals = payload.get("signals")
if not isinstance(signals, list) or not signals:
errors.append("signals 必须是非空数组")
signals = []
functions: set[str] = set()
pads: set[str] = set()
gpios: set[str] = set()
for index, signal in enumerate(signals):
prefix = f"signals[{index}]"
if not isinstance(signal, dict):
errors.append(f"{prefix} 必须是对象")
continue
missing = sorted(REQUIRED_SIGNAL_FIELDS - set(signal))
if missing:
errors.append(f"{prefix} 缺少字段: {', '.join(missing)}")
continue
function = signal.get("function")
if not isinstance(function, str) or not function.strip():
errors.append(f"{prefix}.function 必须是非空字符串")
elif function in functions:
errors.append(f"重复功能名: {function}")
else:
functions.add(function)
direction = signal.get("direction")
decision = signal.get("decision")
active_level = signal.get("active_level")
boot_value = signal.get("boot_value")
pad = signal.get("pad")
gpio = signal.get("gpio")
if direction not in DIRECTIONS:
errors.append(f"{prefix}.direction 非法: {direction!r}")
if active_level not in ACTIVE_LEVELS:
errors.append(f"{prefix}.active_level 非法: {active_level!r}")
if decision not in DECISIONS:
errors.append(f"{prefix}.decision 非法: {decision!r}")
elif decision not in IMPLEMENTABLE and not allow_unconfirmed:
errors.append(f"{prefix} 仍为 {decision},不能进入实现")
if not isinstance(signal.get("runtime_owner"), str):
errors.append(f"{prefix}.runtime_owner 必须是字符串")
if not isinstance(signal.get("note"), str):
errors.append(f"{prefix}.note 必须是字符串")
if not isinstance(signal.get("dts_consumers"), list) or not all(
isinstance(item, str) for item in signal.get("dts_consumers", [])
):
errors.append(f"{prefix}.dts_consumers 必须是字符串数组")
evidence = signal.get("source_evidence")
if not isinstance(evidence, list) or not evidence or not all(
isinstance(item, str) and item.strip() for item in evidence
):
errors.append(f"{prefix}.source_evidence 必须是非空字符串数组")
if decision == "no-gpio":
if direction != "none" or any(
value is not None
for value in (pad, signal.get("mux"), gpio, active_level, boot_value)
):
errors.append(
f"{prefix} 的 no-gpio 行必须使用 direction=none 且 pad/mux/gpio/active_level/boot_value=null"
)
elif direction in {"input", "output"}:
if not isinstance(pad, str) or not pad.strip():
errors.append(f"{prefix} 的 GPIO 行必须填写 pad")
if not isinstance(gpio, str) or not gpio.strip():
errors.append(f"{prefix} 的 GPIO 行必须填写 gpio")
if active_level is None:
errors.append(f"{prefix} 的 GPIO 行不能使用 active_level=null")
if active_level == "unknown" and not allow_unconfirmed:
errors.append(f"{prefix} 的 active_level 仍为 unknown")
if direction == "output" and boot_value not in {0, 1}:
errors.append(f"{prefix} 的输出必须填写 boot_value=0 或 1")
if direction == "input" and boot_value is not None:
errors.append(f"{prefix} 的输入必须使用 boot_value=null")
elif direction == "peripheral":
if not isinstance(pad, str) or not pad.strip():
errors.append(f"{prefix} 的外设复用必须填写 pad")
if gpio is not None or active_level is not None or boot_value is not None:
errors.append(f"{prefix} 的外设复用必须使用 gpio/active_level/boot_value=null")
elif direction == "none" and decision != "no-gpio":
errors.append(f"{prefix} 只有 no-gpio 决策可以使用 direction=none")
if isinstance(pad, str) and pad:
if pad in pads:
errors.append(f"重复 Pad: {pad}")
pads.add(pad)
if isinstance(gpio, str) and gpio:
if gpio in gpios:
errors.append(f"重复 GPIO: {gpio}")
gpios.add(gpio)
if allow_unconfirmed and not confirmed:
warnings.append("计划尚未获得用户确认,仅通过候选结构检查")
return {
"passed": not errors,
"signal_count": len(signals),
"errors": errors,
"warnings": warnings,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("plan", type=Path)
parser.add_argument("--allow-unconfirmed", action="store_true")
args = parser.parse_args()
try:
payload = json.loads(args.plan.read_text(encoding="utf-8"))
result = validate_plan(payload, allow_unconfirmed=args.allow_unconfirmed)
except (OSError, json.JSONDecodeError) as exc:
print(json.dumps({"passed": False, "errors": [str(exc)]}, ensure_ascii=False, indent=2))
return 2
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result["passed"] else 2
if __name__ == "__main__":
raise SystemExit(main())