191 lines
5.2 KiB
Python
191 lines
5.2 KiB
Python
#!/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()
|