"""Seat-specific semantic validation beyond JSON Schema.""" from __future__ import annotations from typing import Any SUPPORTED_TARGETS = {"passenger"} SUPPORTED_FUNCTIONS = {"ventilation", "heating"} SUPPORTED_STATES = {"on", "off"} SUPPORTED_MOTION_AXES = { "longitudinal", "cushion_tilt", "backrest_recline", "legrest_height", "legrest_extension", } ANGLE_RANGES = { "backrest_recline": (95, 163), "legrest_height": (95, 167), } def validate_semantics( parsed: dict[str, Any], function_config: dict[str, Any] | None ) -> list[dict[str, str]]: issues: list[dict[str, str]] = [] detected_domains = set(parsed.get("detected_domains", ["seat"])) if parsed.get("actions") and "seat" not in detected_domains: issues.append( { "severity": "error", "code": "DOMAIN_ACTION_MISMATCH", "path": "detected_domains", "message": "GPT 生成了座椅动作,但 detected_domains 中没有 seat。", } ) for index, deferred in enumerate(parsed.get("deferred_requests", [])): domain = str(deferred.get("domain")) capability = str(deferred.get("capability")) # An N7-synchronised scene may use a vehicle-owned capability namespace # that intentionally differs from its routing domain (for example # ``domain=local_vehicle`` with ``capability=local_scene.seat_preset``). # It is not executed by this validator's local backend: the original # action is forwarded unchanged to ``transto_n7/ctl``. Keep the strict # namespace check for every ordinary deferred request, while allowing # this explicitly marked transfer backend to retain the source contract. n7_transfer = ( deferred.get("execution_backend") == "n7_vehicle_transfer" and deferred.get("rejection_code") == "PC_CAPABILITY_UNAVAILABLE" ) if domain not in detected_domains or ( not n7_transfer and not capability.startswith(f"{domain}.") ): issues.append( { "severity": "error", "code": "DEFERRED_REQUEST_MISMATCH", "path": f"deferred_requests[{index}]", "message": "暂缓请求的领域、能力名和 detected_domains 不一致。", } ) issues.append( { "severity": "warning", "code": "UNAVAILABLE_DOMAIN", "path": f"deferred_requests[{index}]", "message": f"已理解 {capability} 目标,但 {domain} 域当前仅有能力目录,未接入执行器。", } ) for ambiguity in parsed.get("ambiguities", []): issues.append( { "severity": "error", "code": "AMBIGUOUS_INTENT", "path": ambiguity["field"], "message": ambiguity["question"], } ) seen: dict[tuple[str, str], str] = {} active_functions: dict[str, set[str]] = {} has_motion = False for index, action in enumerate(parsed.get("actions", [])): path = f"actions[{index}]" if action.get("target") not in SUPPORTED_TARGETS: issues.append( { "severity": "error", "code": "UNSUPPORTED_TARGET", "path": f"{path}.target", "message": "当前所有座椅能力仅支持前排副驾。", } ) if action.get("action_type") == "dynamic_mode_stop": has_motion = True continue if action.get("action_type") == "dynamic_mode": has_motion = True if action.get("mode_id") not in {1, 2}: issues.append( { "severity": "error", "code": "UNSUPPORTED_DYNAMIC_MODE", "path": f"{path}.mode_id", "message": "动态座椅模式编号必须是 1 或 2。", } ) continue if action.get("action_type") == "position": has_motion = True axis = action.get("axis") position = action.get("position_pct") if axis not in SUPPORTED_MOTION_AXES: issues.append( { "severity": "error", "code": "UNSUPPORTED_MOTION_AXIS", "path": f"{path}.axis", "message": "请求的座椅位置轴未在当前能力目录中注册。", } ) if ( not isinstance(position, int) or isinstance(position, bool) or not 0 <= position <= 100 ): issues.append( { "severity": "error", "code": "UNSUPPORTED_POSITION_RANGE", "path": f"{path}.position_pct", "message": "座椅电机绝对目标必须是 0-100 的整数百分比。", } ) angle = action.get("angle_deg") if angle is not None: angle_range = ANGLE_RANGES.get(str(axis)) if ( angle_range is None or not isinstance(angle, (int, float)) or isinstance(angle, bool) or not angle_range[0] <= float(angle) <= angle_range[1] ): issues.append( { "severity": "error", "code": "UNSUPPORTED_ANGLE_RANGE", "path": f"{path}.angle_deg", "message": "座椅夹角超出当前虚拟标定范围。", } ) key = (str(action.get("target")), str(axis)) previous_position = seen.get(key) if previous_position is not None and previous_position != str(position): issues.append( { "severity": "error", "code": "CONFLICTING_OPERATIONS", "path": path, "message": "同一个座椅电机轴不能在一份配置中包含不同目标位置。", } ) seen[key] = str(position) continue if action.get("function") not in SUPPORTED_FUNCTIONS: issues.append( { "severity": "error", "code": "UNSUPPORTED_CAPABILITY", "path": f"{path}.function", "message": "当前只支持座椅通风和加热。", } ) if action.get("state") not in SUPPORTED_STATES: issues.append( { "severity": "error", "code": "UNSUPPORTED_STATE", "path": f"{path}.state", "message": "当前控制信号只支持打开或关闭。", } ) requested_level = action.get("requested_level") if requested_level is not None and requested_level not in {1, 2, 3}: issues.append( { "severity": "error", "code": "UNSUPPORTED_LEVEL_RANGE", "path": f"{path}.requested_level", "message": "当前舒适推断和 0x4FA 虚拟回读仅支持 1-3 档。", } ) elif requested_level is not None: issues.append( { "severity": "warning", "code": "LEVEL_COMMAND_SIMULATED", "path": f"{path}.requested_level", "message": ( "目标档位已写入 FunctionConfig 和会话状态;" "0x42A 仅编译开关信号,档位回读仅在 dry-run 中模拟。" ), } ) key = (str(action.get("target")), str(action.get("function"))) previous_state = seen.get(key) if previous_state is not None and previous_state != action.get("state"): issues.append( { "severity": "error", "code": "CONFLICTING_OPERATIONS", "path": path, "message": "同一个座椅功能不能在一份配置中同时打开和关闭。", } ) seen[key] = str(action.get("state")) if action.get("state") == "on": active_functions.setdefault(str(action.get("target")), set()).add( str(action.get("function")) ) if has_motion: issues.append( { "severity": "warning", "code": "SYNTHETIC_MOTION_CAN", "path": "platform.reference_vehicle.seat_motion", "message": "座椅位置控制和反馈 CAN 矩阵完全为演示虚构,仅用于验证模型与配置链路。", } ) for target, functions in active_functions.items(): if functions == {"ventilation", "heating"}: issues.append( { "severity": "warning", "code": "COMBINED_THERMAL_SIMULATED", "path": f"actions.{target}", "message": ( "同一座位的加热与通风组合目标仅用于 dry-run;" "实车前必须确认 ECU 是否允许两功能同时开启。" ), } ) if ( function_config is None and not parsed.get("deferred_requests") and not parsed.get("ambiguities") and not parsed.get("preset_actions") ): issues.append( { "severity": "error", "code": "NO_EXECUTABLE_OPERATION", "path": "spec.operations", "message": "没有生成可执行的座椅操作。", } ) issues.append( { "severity": "warning", "code": "VIRTUAL_EXECUTION_ONLY", "path": "platform.reference_vehicle", "message": "CAN 元数据不完整,当前仅允许虚拟执行,禁止实车下发。", } ) return issues