#!/usr/bin/env python3 # -*- coding: utf-8 -*- r""" CM95 → NavSea Mapping Contract 静态检查 v1 检查: 1. mapping 的正式 target_source_layer 是否被当前 style 实际消费; 2. chart_icon_image / chart_fill_pattern / chart_line_pattern 常量是否存在于 semantic sprite; 3. cm95_unmapped 不视为正式 style layer; 4. 输出 PASS/FAIL JSON。 示例: python .\tools\cm95_mapping_contract_check_v1.py ` --mapping .\config\mappings\cm95_to_navsea_v1.yaml ` --style D:\path\style.navsea-delivery-full-semantic-unified-20260808.json ` --sprite-json D:\path\semantic-sprite\sprite.json """ from __future__ import annotations import argparse import json from pathlib import Path import yaml SPRITE_FIELDS = { "chart_icon_image", "chart_fill_pattern", "chart_line_pattern", } def parse_args(): p = argparse.ArgumentParser() p.add_argument("--mapping", required=True) p.add_argument("--style", required=True) p.add_argument("--sprite-json", required=True) p.add_argument("--output", default="cm95_mapping_contract_check_v1.json") return p.parse_args() def const_value(v): if isinstance(v, dict) and set(v.keys()) == {"const"}: return v["const"] if isinstance(v, str): return v return None def collect_sprite_refs(style_fields, path="style_fields"): out = [] if not isinstance(style_fields, dict): return out for k, v in style_fields.items(): p = f"{path}.{k}" if k in SPRITE_FIELDS: c = const_value(v) if isinstance(c, str) and c: out.append((p, c)) if isinstance(v, dict): out.extend(collect_sprite_refs(v, p)) elif isinstance(v, list): for i, item in enumerate(v): if isinstance(item, dict): out.extend( collect_sprite_refs(item, f"{p}[{i}]") ) return out def main(): a = parse_args() mapping = yaml.safe_load( Path(a.mapping).read_text(encoding="utf-8") ) style = json.loads( Path(a.style).read_text(encoding="utf-8") ) sprite = json.loads( Path(a.sprite_json).read_text(encoding="utf-8") ) style_layers = { layer.get("source-layer") for layer in style.get("layers", []) if layer.get("source") == "navsea_delivery" and layer.get("source-layer") } sprite_keys = set(sprite) layer_failures = [] sprite_failures = [] for rule in mapping.get("rules", []): rid = rule.get("rule_id") target = rule.get("target_source_layer") if ( target and target != "cm95_unmapped" and target not in style_layers ): layer_failures.append({ "rule_id": rid, "target_source_layer": target, }) for p, key in collect_sprite_refs( rule.get("style_fields", {}) ): if key not in sprite_keys: sprite_failures.append({ "rule_id": rid, "path": p, "sprite_key": key, }) for i, derived in enumerate( rule.get("derived_outputs", []) or [] ): layer = derived.get("source_layer") if ( layer and layer != "cm95_unmapped" and layer not in style_layers ): layer_failures.append({ "rule_id": rid, "derived_index": i, "target_source_layer": layer, }) for p, key in collect_sprite_refs( derived.get("style_fields", {}), f"derived_outputs[{i}].style_fields", ): if key not in sprite_keys: sprite_failures.append({ "rule_id": rid, "path": p, "sprite_key": key, }) result = { "mapping": str(Path(a.mapping).resolve()), "style": str(Path(a.style).resolve()), "sprite_json": str(Path(a.sprite_json).resolve()), "style_source_layer_count": len(style_layers), "sprite_key_count": len(sprite_keys), "rule_count": len(mapping.get("rules", [])), "layer_failures": layer_failures, "sprite_failures": sprite_failures, "failure_count": ( len(layer_failures) + len(sprite_failures) ), } result["overall"] = ( "PASS" if result["failure_count"] == 0 else "FAIL" ) Path(a.output).write_text( json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8", ) print( f"Rules : {result['rule_count']}" ) print( f"Style source layers : {result['style_source_layer_count']}" ) print( f"Sprite keys : {result['sprite_key_count']}" ) print( f"Layer failures : {len(layer_failures)}" ) print( f"Sprite failures : {len(sprite_failures)}" ) print( f"OVERALL : {result['overall']}" ) if __name__ == "__main__": main()