#!/usr/bin/env python3 """Validate 2026.7 hierarchical fast/slow routing records.""" from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any POLICY_VERSION = "FAST-SLOW-2026.7" SKILL_ROOT = Path(__file__).resolve().parent.parent CONFLICT_FILE = SKILL_ROOT / "references" / "policy-conflicts.md" DEFAULT_ROUTE_BY_LABEL = { "快": "快系统", "慢": "慢系统", "模糊": "慢系统", } CONFIDENCE_VALUES = {"high", "medium", "low"} DECISION_LEVELS = {"domain", "structure", "core"} DOMAIN_VALUES = { "导航/生服", "车控", "产品问答", "内容", "系统/App控制", "通用工具", "IoT", "自动化", } RULE_ID_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]+$") CONFLICT_ID_PATTERN = re.compile(r"^CONFLICT_[A-Z0-9_]+$") REQUIRED_FIELDS = { "query", "label", "default_route", "domain", "candidate_domains", "decision_level", "decision_rule_id", "decision_source", "decision_path", "reason", "evidence", "structural_signals", "context_used", "fast_system_pending", "route_override", "policy_conflict", "conflict_refs", "confidence", "review_required", "uncertainties", "policy_version", } OPTIONAL_FIELDS = { "id", "counterevidence", "context_relation", } CONTEXT_RELATION_VALUES = { "independent", "resolved_direct", "operation_continuation", "constraint_extension", "planning_continuation", "clarification_completion", "unresolved_reference", } def _is_nonempty_string(value: Any) -> bool: return isinstance(value, str) and bool(value.strip()) def _check_string_list( value: Any, *, field: str, errors: list[str], require_nonempty: bool = False, unique: bool = False, ) -> None: if not isinstance(value, list): errors.append(f"{field} 必须是数组") return if require_nonempty and not value: errors.append(f"{field} 至少包含一项") if any(not _is_nonempty_string(item) for item in value): errors.append(f"{field} 的每一项都必须是非空字符串") if unique and all(isinstance(item, str) for item in value): if len(value) != len(set(value)): errors.append(f"{field} 不允许重复项") def _validate_decision_source( record: dict[str, Any], *, errors: list[str], ) -> None: source = record.get("decision_source") rule_id = record.get("decision_rule_id") level = record.get("decision_level") if not _is_nonempty_string(source): errors.append("decision_source 必须是非空字符串") return relative_path = source.split("#", 1)[0] if not relative_path.startswith("references/") or not relative_path.endswith(".md"): errors.append("decision_source 必须指向 references/*.md") return resolved = (SKILL_ROOT / relative_path).resolve() references_root = (SKILL_ROOT / "references").resolve() if references_root not in resolved.parents: errors.append("decision_source 不允许跳出 references 目录") return if not resolved.is_file(): errors.append(f"decision_source 文件不存在: {relative_path}") return try: source_text = resolved.read_text(encoding="utf-8") except OSError as exc: errors.append(f"无法读取 decision_source: {exc}") return if _is_nonempty_string(rule_id) and rule_id not in source_text: errors.append(f"decision_rule_id 未出现在来源文件中: {rule_id}") filename = resolved.name if level == "domain" and not filename.startswith("domain-"): errors.append("decision_level=domain 时来源文件必须是 domain-*.md") if level == "structure" and not filename.startswith("structure-"): errors.append("decision_level=structure 时来源文件必须是 structure-*.md") if level == "core" and filename != "core-policy.md": errors.append("decision_level=core 时来源文件必须是 core-policy.md") def _validate_conflicts( record: dict[str, Any], *, errors: list[str], ) -> None: policy_conflict = record.get("policy_conflict") conflict_refs = record.get("conflict_refs") if not isinstance(policy_conflict, bool): errors.append("policy_conflict 必须是布尔值") _check_string_list( conflict_refs, field="conflict_refs", errors=errors, unique=True, ) if not isinstance(conflict_refs, list): return if policy_conflict is True and not conflict_refs: errors.append("policy_conflict=true 时 conflict_refs 至少包含一项") if policy_conflict is False and conflict_refs: errors.append("policy_conflict=false 时 conflict_refs 必须为空") try: known_conflicts = CONFLICT_FILE.read_text(encoding="utf-8") except OSError as exc: errors.append(f"无法读取冲突表: {exc}") return for conflict_id in conflict_refs: if not isinstance(conflict_id, str): continue if not CONFLICT_ID_PATTERN.fullmatch(conflict_id): errors.append(f"冲突 ID 格式非法: {conflict_id!r}") elif conflict_id not in known_conflicts: errors.append(f"冲突 ID 未登记: {conflict_id}") def validate_record(record: Any, index: int) -> dict[str, Any]: errors: list[str] = [] warnings: list[str] = [] if not isinstance(record, dict): return { "index": index, "valid": False, "errors": ["记录必须是 JSON 对象"], "warnings": [], } if "__parse_error__" in record: return { "index": index, "valid": False, "errors": [str(record["__parse_error__"])], "warnings": [], } missing = sorted(REQUIRED_FIELDS - set(record)) if missing: errors.append(f"缺少必填字段: {', '.join(missing)}") unknown = sorted(set(record) - REQUIRED_FIELDS - OPTIONAL_FIELDS) if unknown: errors.append(f"存在未知字段: {', '.join(unknown)}") if not _is_nonempty_string(record.get("query")): errors.append("query 必须是非空字符串") label = record.get("label") if not isinstance(label, str) or label not in DEFAULT_ROUTE_BY_LABEL: errors.append("label 必须是 快、慢、模糊 之一") expected_route = DEFAULT_ROUTE_BY_LABEL.get(label) if isinstance(label, str) else None if expected_route is not None and record.get("default_route") != expected_route: errors.append(f"label={label} 时 default_route 必须是 {expected_route}") domain = record.get("domain") if domain is not None and ( not isinstance(domain, str) or domain not in DOMAIN_VALUES ): errors.append("domain 不是受支持的粗粒度垂域") _check_string_list( record.get("candidate_domains"), field="candidate_domains", errors=errors, unique=True, ) candidate_domains = record.get("candidate_domains") if isinstance(candidate_domains, list): unknown_domains = [ value for value in candidate_domains if not isinstance(value, str) or value not in DOMAIN_VALUES ] if unknown_domains: errors.append(f"candidate_domains 包含未知垂域: {unknown_domains}") level = record.get("decision_level") if not isinstance(level, str) or level not in DECISION_LEVELS: errors.append("decision_level 必须是 domain、structure、core 之一") if level == "domain" and domain is None: errors.append("decision_level=domain 时 domain 不能为空") rule_id = record.get("decision_rule_id") if not _is_nonempty_string(rule_id) or not RULE_ID_PATTERN.fullmatch(rule_id): errors.append("decision_rule_id 必须是大写下划线规则 ID") _check_string_list( record.get("decision_path"), field="decision_path", errors=errors, require_nonempty=True, ) decision_path = record.get("decision_path") if ( isinstance(decision_path, list) and _is_nonempty_string(rule_id) and not any(rule_id in str(item) for item in decision_path) ): errors.append("decision_path 必须包含 decision_rule_id") _validate_decision_source(record, errors=errors) if not _is_nonempty_string(record.get("reason")): errors.append("reason 必须是非空字符串") _check_string_list( record.get("evidence"), field="evidence", errors=errors, require_nonempty=True, ) if "counterevidence" in record: _check_string_list( record.get("counterevidence"), field="counterevidence", errors=errors, ) structural_signals = record.get("structural_signals") expected_signal_keys = { "complex", "multi_instruction", "auto_task", "context_dependent", } if not isinstance(structural_signals, dict): errors.append("structural_signals 必须是对象") else: actual_keys = set(structural_signals) if actual_keys != expected_signal_keys: errors.append( "structural_signals 必须且只能包含 " "complex、multi_instruction、auto_task、context_dependent" ) for key in expected_signal_keys: value = structural_signals.get(key) if value is not None and not isinstance(value, bool): errors.append(f"structural_signals.{key} 必须是布尔值或 null") if not isinstance(record.get("context_used"), bool): errors.append("context_used 必须是布尔值") context_relation = record.get("context_relation") if context_relation is not None and ( not isinstance(context_relation, str) or context_relation not in CONTEXT_RELATION_VALUES ): errors.append("context_relation 不是受支持的上下文关系") if context_relation == "independent" and record.get("context_used") is True: errors.append("context_relation=independent 时 context_used 必须为 false") if context_relation not in {None, "independent"}: if record.get("context_used") is not True: errors.append("依赖上下文的 context_relation 要求 context_used=true") if isinstance(structural_signals, dict) and ( structural_signals.get("context_dependent") is not True ): errors.append( "依赖上下文的 context_relation 要求 " "structural_signals.context_dependent=true" ) fast_system_pending = record.get("fast_system_pending") if fast_system_pending is not None and not isinstance(fast_system_pending, bool): errors.append("fast_system_pending 必须是布尔值或 null") if fast_system_pending is True and label != "快": errors.append("只有 label=快 时 fast_system_pending 才能为 true") route_override = record.get("route_override") if route_override is not None and not _is_nonempty_string(route_override): errors.append("route_override 必须是非空字符串或 null") _validate_conflicts(record, errors=errors) confidence = record.get("confidence") if not isinstance(confidence, str) or confidence not in CONFIDENCE_VALUES: errors.append("confidence 必须是 high、medium、low 之一") review_required = record.get("review_required") if not isinstance(review_required, bool): errors.append("review_required 必须是布尔值") if confidence == "low" and review_required is not True: errors.append("confidence=low 时 review_required 必须为 true") _check_string_list( record.get("uncertainties"), field="uncertainties", errors=errors, ) uncertainties = record.get("uncertainties") if isinstance(uncertainties, list) and uncertainties and review_required is False: warnings.append("uncertainties 非空但 review_required=false,请确认不确定点不影响标签") if record.get("policy_version") != POLICY_VERSION: errors.append(f"policy_version 必须是 {POLICY_VERSION}") return { "index": index, "id": record.get("id"), "valid": not errors, "errors": errors, "warnings": warnings, } def _load_records_from_file(path: Path) -> list[Any]: text = path.read_text(encoding="utf-8-sig") if path.suffix.lower() == ".jsonl": records: list[Any] = [] for line_number, line in enumerate(text.splitlines(), start=1): if not line.strip(): continue try: records.append(json.loads(line)) except json.JSONDecodeError as exc: records.append( { "__parse_error__": ( f"JSONL 第 {line_number} 行解析失败: {exc.msg}" ) } ) return records payload = json.loads(text) if isinstance(payload, list): return payload if isinstance(payload, dict) and isinstance(payload.get("records"), list): return payload["records"] return [payload] def _parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="校验 FAST-SLOW-2026.7 分层打标 JSON/JSONL" ) source_group = parser.add_mutually_exclusive_group(required=True) source_group.add_argument("--record", help="单条 JSON 对象字符串") source_group.add_argument("--file", type=Path, help="JSON 或 JSONL 文件路径") parser.add_argument( "--strict", action="store_true", help="把 warning 也视为校验失败", ) return parser.parse_args() def main() -> int: args = _parse_args() try: if args.record is not None: records = [json.loads(args.record)] else: records = _load_records_from_file(args.file) except (OSError, json.JSONDecodeError) as exc: result = { "valid": False, "total": 0, "invalid": 1, "warning_count": 0, "errors": [str(exc)], "results": [], } print(json.dumps(result, ensure_ascii=False, indent=2)) return 1 results = [validate_record(record, index) for index, record in enumerate(records)] invalid = sum(not result["valid"] for result in results) warning_count = sum(len(result["warnings"]) for result in results) valid = invalid == 0 and (not args.strict or warning_count == 0) summary = { "valid": valid, "total": len(records), "invalid": invalid, "warning_count": warning_count, "results": results, } print(json.dumps(summary, ensure_ascii=False, indent=2)) return 0 if valid else 1 if __name__ == "__main__": sys.exit(main())