Files
linuxcncWebWASM/scripts/generate_kinematics_mapping_contract.py

66 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Record deterministic axis/joint, duplicate-axis, home and limit semantics."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
header = root / "wasm-core/include/sim_kinematics.h"
source = root / "wasm-core/src/sim_kinematics.c"
report = {
"schema": "kinematics-mapping-contract.v1",
"context_id": json.loads((root / "context-lock.json").read_text(encoding="utf-8")).get("context_id"),
"inputs": {"header": header.as_posix(), "header_sha256": sha256(header), "source": source.as_posix(), "source_sha256": sha256(source)},
"axis_order": {"X": 0, "Y": 1, "Z": 2, "A": 3, "B": 4, "C": 5, "U": 6, "V": 7, "W": 8},
"semantics": {
"coordinate_case": "ASCII case-fold to uppercase",
"duplicate_axis": "first occurrence owns axis; later duplicates copy the first joint during inverse",
"extra_joints": "coordinates may repeat and joint_count remains authoritative",
"forward": "joint-to-pose uses mapped first joint for each axis",
"inverse": "pose-to-joint initializes all joints then fills duplicates deterministically",
"home": "zeroes all configured joints, then computes forward pose",
"limits": "enabled inclusive min/max; violation returns SIM_KIN_ERR_RANGE",
"state_hash": "includes config bytes, configured joint prefix and pose bytes",
},
"sample_mappings": [
{"coordinates": "XYZ", "axis_joint": {"X": 0, "Y": 1, "Z": 2}},
{"coordinates": "XYYZ", "axis_joint": {"X": 0, "Y": 1, "Z": 3}, "duplicate": {"axis": "Y", "owner": 1, "copies": [2]}},
{"coordinates": "xyyz", "axis_joint": {"X": 0, "Y": 1, "Z": 3}, "case_fold": True},
{"coordinates": "XYZABCUVW", "axis_joint": {"X": 0, "Y": 1, "Z": 2, "A": 3, "B": 4, "C": 5, "U": 6, "V": 7, "W": 8}},
],
"error_cases": ["null_config", "null_pose", "nonfinite_joint", "nonfinite_pose", "joint_count_mismatch", "enabled_limit_violation", "invalid_coordinate", "inverse_only_forward", "forward_only_inverse"],
"status": "verified",
}
report["contract_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/kinematics-mapping-contract.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"kinematics_mapping={report['status']} samples={len(report['sample_mappings'])} errors={len(report['error_cases'])}")
return 0
if __name__ == "__main__":
raise SystemExit(main())