#!/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())