Extend simulation runtime coverage and kinematics support
This commit is contained in:
415
scripts/generate_configuration_regression.py
Normal file
415
scripts/generate_configuration_regression.py
Normal file
@@ -0,0 +1,415 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the deterministic K8 full-configuration regression report.
|
||||
|
||||
The LinuxCNC checkout used by this project is intentionally a source/config
|
||||
fixture rather than a complete desktop installation. This report therefore
|
||||
separates a deterministic static startup preflight from the native/WASM and
|
||||
Worker runtime probes. Missing resources and unavailable official oracles
|
||||
remain explicit blockers; they are never converted into passing goldens.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
CONTEXT_PATH = Path("context-lock.json")
|
||||
GRAPH_PATH = Path("coverage/kinematics-config-graph.report.json")
|
||||
RESOURCE_PATH = Path("coverage/config-resources.jsonl")
|
||||
RESOURCE_REPORT_PATH = Path("coverage/config-resources.report.json")
|
||||
RECOVERY_PATH = Path("coverage/workspace-recovery.report.json")
|
||||
REGISTRY_PATH = Path("coverage/kinematics-registry.report.json")
|
||||
|
||||
RUNTIME_COMMANDS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("kinematics", ("build-native/sim_kinematics_test",)),
|
||||
("motion", ("build-native/sim_kinematics_motion_test",)),
|
||||
("feedback", ("build-native/sim_kinematics_feedback_test",)),
|
||||
("home_limit", ("build-native/sim_kinematics_home_limit_test",)),
|
||||
("io", ("build-native/sim_io_test",)),
|
||||
)
|
||||
WASM_COMMANDS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("kinematics", ("node", "build-wasm/sim_kinematics_wasm_test.js")),
|
||||
("motion", ("node", "build-wasm/sim_kinematics_motion_wasm_test.js")),
|
||||
("feedback", ("node", "build-wasm/sim_kinematics_feedback_wasm_test.js")),
|
||||
("home_limit", ("node", "build-wasm/sim_kinematics_home_limit_wasm_test.js")),
|
||||
("io", ("node", "build-wasm/sim_io_wasm_test.js")),
|
||||
)
|
||||
WORKER_COMMANDS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("replay", ("node", "web/worker/test_replay.mjs")),
|
||||
("workspace_recovery", ("node", "web/workspace/test_recovery.mjs")),
|
||||
("sim_session", ("node", "web/worker/test_sim_session.mjs")),
|
||||
)
|
||||
UNRESOLVED_STATUSES = {"unresolved", "host_or_variable_path", "outside_repo"}
|
||||
KINEMATICS_SAMPLE_KEYS = {
|
||||
"corexykins": "corexy",
|
||||
"rosekins": "rose",
|
||||
"maxkins": "maxkins",
|
||||
"rotatekins": "rotatekins",
|
||||
"5axiskins": "fiveaxiskins",
|
||||
"xyzac-trt-kins": "xyzac_trt",
|
||||
"xyzbc-trt-kins": "xyzbc_trt",
|
||||
"scarakins": "scara",
|
||||
"pumakins": "puma",
|
||||
"scorbot-kins": "scorbot",
|
||||
"genserkins": "genser",
|
||||
"lineardeltakins": "lineardelta",
|
||||
"rotarydeltakins": "rotarydelta",
|
||||
"tripodkins": "tripod",
|
||||
"genhexkins": "genhex",
|
||||
"pentakins": "pentapod",
|
||||
}
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def digest(value: Any) -> str:
|
||||
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
handle = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
handle.update(block)
|
||||
return handle.hexdigest()
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path} must contain an object")
|
||||
return value
|
||||
|
||||
|
||||
def read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{path}:{line_number} must contain an object")
|
||||
rows.append(value)
|
||||
return rows
|
||||
|
||||
|
||||
def source_record(root: Path, path: Path) -> dict[str, Any]:
|
||||
relative = path.relative_to(root).as_posix()
|
||||
return {"path": relative, "sha256": sha256(path)} if path.is_file() else {"path": relative, "sha256": None, "status": "missing"}
|
||||
|
||||
|
||||
def parse_ini_shape(path: Path) -> dict[str, Any]:
|
||||
"""Check only syntax needed by the preflight; it is not an INI oracle."""
|
||||
if not path.is_file():
|
||||
return {"file": False, "sections": [], "balanced_sections": False}
|
||||
sections: list[str] = []
|
||||
balanced = True
|
||||
for raw in path.read_text(encoding="utf-8", errors="strict").splitlines():
|
||||
line = raw.split("#", 1)[0].split(";", 1)[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("["):
|
||||
if not line.endswith("]"):
|
||||
balanced = False
|
||||
else:
|
||||
sections.append(line[1:-1].strip().upper())
|
||||
return {"file": True, "sections": sorted(set(sections)), "balanced_sections": balanced}
|
||||
|
||||
|
||||
def registered_kinematics(root: Path) -> set[str]:
|
||||
registry = read_json(REGISTRY_PATH if REGISTRY_PATH.is_absolute() else root / REGISTRY_PATH)
|
||||
rows = registry.get("samples", {}).get("native", {}).get("entries", [])
|
||||
return {str(row.get("name")) for row in rows if row.get("name")}
|
||||
|
||||
|
||||
def graph_entries(root: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
graph = read_json(root / GRAPH_PATH)
|
||||
entries = [row for row in graph.get("nodes", []) if row.get("kind") == "kinematics"]
|
||||
entries.sort(key=lambda row: str(row.get("node_id", "")))
|
||||
return graph, entries
|
||||
|
||||
|
||||
def resource_summaries(root: Path, resource_report: dict[str, Any], rows: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
||||
by_entrypoint: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for row in rows:
|
||||
by_entrypoint[str(row.get("entrypoint", ""))].append(row)
|
||||
indexed = {str(path) for path in resource_report.get("entrypoint_paths", [])}
|
||||
result: dict[str, list[dict[str, Any]]] = {}
|
||||
for path in sorted(indexed):
|
||||
result[path] = by_entrypoint.get(path, [])
|
||||
return result
|
||||
|
||||
|
||||
def preflight_entry(root: Path, row: dict[str, Any], resources: dict[str, list[dict[str, Any]]], indexed_paths: set[str], registered: set[str]) -> dict[str, Any]:
|
||||
path = str(row["path"])
|
||||
module = str(row.get("module", ""))
|
||||
resource_rows = resources.get(path, [])
|
||||
missing = sorted({str(item.get("path")) for item in resource_rows if item.get("parse_status") == "missing"})
|
||||
unresolved = sorted({str(item.get("path")) for item in resource_rows if item.get("parse_status") in UNRESOLVED_STATUSES})
|
||||
ini_shape = parse_ini_shape(root / path)
|
||||
if module not in registered:
|
||||
startup_status = "blocked_unregistered_kinematics"
|
||||
elif path not in indexed_paths and path.startswith("linuxcnc/configs/sim/"):
|
||||
startup_status = "blocked_unindexed_resource_graph"
|
||||
elif missing or unresolved:
|
||||
startup_status = "blocked_resource"
|
||||
elif not ini_shape["file"] or not ini_shape["balanced_sections"]:
|
||||
startup_status = "blocked_ini_syntax"
|
||||
else:
|
||||
startup_status = "static_ready"
|
||||
if path not in indexed_paths:
|
||||
resource_status = "unindexed"
|
||||
elif missing and unresolved:
|
||||
resource_status = "blocked_missing_and_unresolved"
|
||||
elif missing:
|
||||
resource_status = "blocked_missing"
|
||||
elif unresolved:
|
||||
resource_status = "blocked_unresolved"
|
||||
else:
|
||||
resource_status = "ready"
|
||||
return {
|
||||
"config_id": row.get("node_id"),
|
||||
"path": path,
|
||||
"line": row.get("line"),
|
||||
"module": module,
|
||||
"family": row.get("family"),
|
||||
"resource_status": resource_status,
|
||||
"missing_resources": missing,
|
||||
"unresolved_resources": unresolved,
|
||||
"resource_rows": len(resource_rows),
|
||||
"ini": ini_shape,
|
||||
"kinematics_registered": module in registered,
|
||||
"startup_preflight": startup_status,
|
||||
}
|
||||
|
||||
|
||||
def run_json_command(root: Path, command: tuple[str, ...]) -> dict[str, Any]:
|
||||
executable = root / command[0] if command[0] not in {"node"} else root / command[1]
|
||||
if not executable.exists():
|
||||
return {"status": "blocked", "reason": f"missing_runtime_artifact:{executable.relative_to(root).as_posix()}"}
|
||||
completed = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
if completed.returncode != 0:
|
||||
return {"status": "failed", "exit_code": completed.returncode, "stderr_sha256": digest(completed.stderr)}
|
||||
json_lines = [line.strip() for line in completed.stdout.splitlines() if line.lstrip().startswith("{")]
|
||||
if not json_lines:
|
||||
return {"status": "failed", "reason": "runtime_did_not_emit_json", "stdout_sha256": digest(completed.stdout)}
|
||||
try:
|
||||
sample = json.loads(json_lines[-1])
|
||||
except json.JSONDecodeError:
|
||||
return {"status": "failed", "reason": "runtime_emitted_invalid_json", "stdout_sha256": digest(completed.stdout)}
|
||||
return {"status": "passed", "schema": sample.get("schema"), "sample": sample, "sample_sha256": digest(sample)}
|
||||
|
||||
|
||||
def run_runtime_group(root: Path, commands: Iterable[tuple[str, tuple[str, ...]]]) -> dict[str, Any]:
|
||||
records: dict[str, Any] = {}
|
||||
for name, command in commands:
|
||||
first = run_json_command(root, command)
|
||||
second = run_json_command(root, command)
|
||||
first_key = first.get("sample_sha256")
|
||||
second_key = second.get("sample_sha256")
|
||||
records[name] = {
|
||||
"command": list(command),
|
||||
"first": first,
|
||||
"second": second,
|
||||
"repeat_stable": bool(first_key and first_key == second_key),
|
||||
}
|
||||
passed = all(row["first"].get("status") == "passed" and row["repeat_stable"] for row in records.values())
|
||||
return {"status": "passed" if passed else "blocked", "records": records, "summary_sha256": digest(records)}
|
||||
|
||||
|
||||
def run_worker_group(root: Path) -> dict[str, Any]:
|
||||
records: dict[str, Any] = {}
|
||||
for name, command in WORKER_COMMANDS:
|
||||
target = root / command[-1]
|
||||
if not target.is_file():
|
||||
records[name] = {"command": list(command), "status": "blocked", "reason": "missing_worker_test"}
|
||||
continue
|
||||
first = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
second = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
first_value = {"exit_code": first.returncode, "stdout": first.stdout, "stderr": first.stderr}
|
||||
second_value = {"exit_code": second.returncode, "stdout": second.stdout, "stderr": second.stderr}
|
||||
records[name] = {
|
||||
"command": list(command),
|
||||
"status": "passed" if first.returncode == 0 else "failed",
|
||||
"first_sha256": digest(first_value),
|
||||
"second_sha256": digest(second_value),
|
||||
"repeat_stable": digest(first_value) == digest(second_value),
|
||||
"stdout": first.stdout.strip(),
|
||||
}
|
||||
passed = all(value.get("status") == "passed" and value.get("repeat_stable") for value in records.values())
|
||||
return {"status": "passed" if passed else "blocked", "records": records, "summary_sha256": digest(records)}
|
||||
|
||||
|
||||
def extract_state_hashes(value: Any, path: str = "") -> list[dict[str, Any]]:
|
||||
found: list[dict[str, Any]] = []
|
||||
if isinstance(value, dict):
|
||||
for key, child in sorted(value.items()):
|
||||
child_path = f"{path}.{key}" if path else key
|
||||
if key in {"state_hash", "hash", "repeat_hash"} and isinstance(child, (int, str)):
|
||||
found.append({"path": child_path, "value": child})
|
||||
found.extend(extract_state_hashes(child, child_path))
|
||||
elif isinstance(value, list):
|
||||
for index, child in enumerate(value):
|
||||
found.extend(extract_state_hashes(child, f"{path}[{index}]"))
|
||||
return found
|
||||
|
||||
|
||||
def build_config_state_summary(entries: list[dict[str, Any]], runtime: dict[str, Any], registry: set[str]) -> dict[str, Any]:
|
||||
samples: dict[str, Any] = {}
|
||||
for name, record in runtime.get("records", {}).items():
|
||||
sample = record.get("first", {}).get("sample")
|
||||
if isinstance(sample, dict):
|
||||
samples[name] = {
|
||||
"schema": sample.get("schema"),
|
||||
"sample_sha256": record["first"].get("sample_sha256"),
|
||||
"state_hashes": extract_state_hashes(sample),
|
||||
}
|
||||
rows = []
|
||||
for entry in entries:
|
||||
module = str(entry.get("module", ""))
|
||||
sample_key = KINEMATICS_SAMPLE_KEYS.get(module)
|
||||
rows.append({
|
||||
"config_id": entry.get("config_id"),
|
||||
"module": module,
|
||||
"sample": sample_key,
|
||||
"sample_status": "verified_sample" if sample_key in samples else "registry_only" if module in registry else "blocked_unregistered",
|
||||
"state_digest": digest({"module": module, "sample": samples.get(sample_key), "registry": module in registry}),
|
||||
})
|
||||
rows.sort(key=lambda row: str(row["config_id"]))
|
||||
return {"tick_policy": "fixed logical tick; wall clock excluded", "entries": rows, "runtime_samples": samples, "summary_sha256": digest(rows)}
|
||||
|
||||
|
||||
def generate(root: Path, output: Path) -> dict[str, Any]:
|
||||
root = root.resolve()
|
||||
context = read_json(root / CONTEXT_PATH)
|
||||
graph, entries = graph_entries(root)
|
||||
resource_report = read_json(root / RESOURCE_REPORT_PATH)
|
||||
resources_rows = read_jsonl(root / RESOURCE_PATH)
|
||||
resources = resource_summaries(root, resource_report, resources_rows)
|
||||
indexed_paths = set(resources)
|
||||
registered = registered_kinematics(root)
|
||||
preflight = [preflight_entry(root, row, resources, indexed_paths, registered) for row in entries]
|
||||
preflight.sort(key=lambda row: str(row["config_id"]))
|
||||
runtime = run_runtime_group(root, RUNTIME_COMMANDS)
|
||||
wasm = run_runtime_group(root, WASM_COMMANDS)
|
||||
worker = run_worker_group(root)
|
||||
native_wasm_equal = all(
|
||||
runtime["records"].get(name, {}).get("first", {}).get("sample_sha256")
|
||||
== wasm["records"].get(name, {}).get("first", {}).get("sample_sha256")
|
||||
and runtime["records"].get(name, {}).get("first", {}).get("status") == "passed"
|
||||
for name, _command in RUNTIME_COMMANDS
|
||||
)
|
||||
graph_orphans = list(graph.get("closure", {}).get("orphan_entry_ids", []))
|
||||
unowned = list(graph.get("closure", {}).get("unowned_template_ids", []))
|
||||
resource_orphans = sorted({str(row.get("entrypoint")) for row in resources_rows if str(row.get("entrypoint")) not in indexed_paths})
|
||||
startup_counts: dict[str, int] = defaultdict(int)
|
||||
resource_counts: dict[str, int] = defaultdict(int)
|
||||
for row in preflight:
|
||||
startup_counts[str(row["startup_preflight"])] += 1
|
||||
resource_counts[str(row["resource_status"])] += 1
|
||||
missing_resource_paths = sorted({path for row in preflight for path in row["missing_resources"]})
|
||||
unresolved_resource_paths = sorted({path for row in preflight for path in row["unresolved_resources"]})
|
||||
recovery = read_json(root / RECOVERY_PATH)
|
||||
state_summary = build_config_state_summary(entries, runtime, registered)
|
||||
report: dict[str, Any] = {
|
||||
"schema": "configuration-regression-report.v1",
|
||||
"context_id": context.get("context_id"),
|
||||
"status": "partial",
|
||||
"implementation_status": "verified",
|
||||
"scope": {"configs_root": "linuxcnc/configs/sim", "tests_root": "linuxcnc/tests", "preflight_only": True},
|
||||
"inputs": {
|
||||
# The report is itself pinned by context-lock. Recording only the
|
||||
# context ID here avoids a circular lock/report content hash.
|
||||
"context_lock": {"path": CONTEXT_PATH.as_posix(), "context_id": context.get("context_id")},
|
||||
"kinematics_graph": source_record(root, root / GRAPH_PATH),
|
||||
"config_resources": source_record(root, root / RESOURCE_PATH),
|
||||
"config_resources_report": source_record(root, root / RESOURCE_REPORT_PATH),
|
||||
"workspace_recovery": source_record(root, root / RECOVERY_PATH),
|
||||
"kinematics_registry": source_record(root, root / REGISTRY_PATH),
|
||||
},
|
||||
"counts": {
|
||||
"registered_configuration_entries": len(entries),
|
||||
"config_entrypoints": int(resource_report.get("entrypoints", 0)),
|
||||
"resource_rows": len(resources_rows),
|
||||
"resource_missing_rows": sum(1 for row in resources_rows if row.get("parse_status") == "missing"),
|
||||
"resource_unresolved_rows": sum(1 for row in resources_rows if row.get("parse_status") in UNRESOLVED_STATUSES),
|
||||
"missing_resource_paths": len(missing_resource_paths),
|
||||
"unresolved_resource_paths": len(unresolved_resource_paths),
|
||||
"resource_orphans": len(resource_orphans),
|
||||
"graph_orphans": len(graph_orphans),
|
||||
"unowned_templates": len(unowned),
|
||||
"startup_preflight": dict(sorted(startup_counts.items())),
|
||||
"resource_status": dict(sorted(resource_counts.items())),
|
||||
},
|
||||
"configuration_entries": preflight,
|
||||
"closure": {
|
||||
"graph_fingerprint_sha256": graph.get("graph_fingerprint_sha256"),
|
||||
"graph_orphan_entry_ids": graph_orphans,
|
||||
"unowned_template_ids": unowned,
|
||||
"resource_orphan_entrypoints": resource_orphans,
|
||||
"all_registered": len(entries) == int(graph.get("counts", {}).get("configuration_entries", -1)),
|
||||
},
|
||||
"runtime": {
|
||||
"native": runtime,
|
||||
"wasm": wasm,
|
||||
"native_wasm": {"status": "passed" if native_wasm_equal else "blocked", "sample_hashes_equal": native_wasm_equal},
|
||||
"worker": worker,
|
||||
},
|
||||
"state_summary": state_summary,
|
||||
"recovery_summary": {
|
||||
"status": recovery.get("status"),
|
||||
"implementation_status": recovery.get("implementation_status"),
|
||||
"checkpoint_schema": recovery.get("replay", {}).get("schema"),
|
||||
"replay_state_hash": recovery.get("replay", {}).get("state_hash"),
|
||||
"continuous_state_hash": recovery.get("replay", {}).get("continuous_state_hash"),
|
||||
"endpoints_equal": recovery.get("replay", {}).get("endpoints_equal"),
|
||||
"oracle_status": recovery.get("oracle", {}).get("status"),
|
||||
},
|
||||
"oracle": {
|
||||
"status": "blocked",
|
||||
"reason": "official LinuxCNC kinematics/recovery oracle is absent from the locked source",
|
||||
"official_root": "linuxcnc/unit_tests/kinematics",
|
||||
"derived_configuration_results_are_not_official": True,
|
||||
},
|
||||
"unclosed": {
|
||||
"resource_missing": missing_resource_paths,
|
||||
"resource_unresolved": unresolved_resource_paths,
|
||||
"unregistered_kinematics": sorted({str(row["module"]) for row in preflight if not row["kinematics_registered"]}),
|
||||
"startup_blocked_entries": [row["config_id"] for row in preflight if str(row["startup_preflight"]).startswith("blocked")],
|
||||
},
|
||||
"report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
|
||||
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/configuration-regression.report.json"))
|
||||
args = parser.parse_args()
|
||||
report = generate(args.repo_root, args.repo_root / args.output)
|
||||
print(
|
||||
"configuration_regression="
|
||||
f"entries={report['counts']['registered_configuration_entries']} "
|
||||
f"startup_blocked={len(report['unclosed']['startup_blocked_entries'])} "
|
||||
f"resource_orphans={report['counts']['resource_orphans']} "
|
||||
f"native_wasm={report['runtime']['native_wasm']['status']} "
|
||||
f"worker={report['runtime']['worker']['status']}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
113
scripts/generate_io_report.py
Normal file
113
scripts/generate_io_report.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate deterministic evidence for spindle, tool changer and probe IO."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
INPUTS = {
|
||||
"io_header": "wasm-core/include/sim_io.h",
|
||||
"io_source": "wasm-core/src/sim_io.c",
|
||||
"hal_header": "wasm-core/include/sim_hal.h",
|
||||
"hal_source": "wasm-core/src/sim_hal.c",
|
||||
"test": "wasm-core/tests/sim_io_test.c",
|
||||
"linuxcnc_motion": "linuxcnc/src/emc/motion/motion.c",
|
||||
"linuxcnc_control": "linuxcnc/src/emc/motion/control.c",
|
||||
"linuxcnc_iocontrol": "linuxcnc/src/emc/iotask/ioControl.cc",
|
||||
"linuxcnc_iocontrol_v2": "linuxcnc/src/emc/iotask/ioControl_v2.cc",
|
||||
"linuxcnc_sim_spindle": "linuxcnc/src/hal/components/sim_spindle.comp",
|
||||
}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def run_json(command: list[str], root: Path) -> dict[str, object]:
|
||||
result = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"{command[0]} exited {result.returncode}: {result.stderr.strip()}")
|
||||
for line in result.stdout.splitlines():
|
||||
if line.lstrip().startswith("{"):
|
||||
item = json.loads(line)
|
||||
if item.get("schema") == "io-samples.v1":
|
||||
return item
|
||||
raise RuntimeError("runner did not emit io-samples.v1")
|
||||
|
||||
|
||||
def canonical(value: object) -> str:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def generate(root: Path, output: Path) -> dict[str, object]:
|
||||
root = root.resolve()
|
||||
native = run_json([str(root / "build-native/sim_io_test")], root)
|
||||
wasm = run_json(["node", str(root / "build-wasm/sim_io_wasm_test.js")], root)
|
||||
report = {
|
||||
"schema": "io-runtime-report.v1",
|
||||
"status": "partial",
|
||||
"implementation_status": "verified",
|
||||
"source_records": {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()},
|
||||
"registration": {
|
||||
"spindle_capacity": 8,
|
||||
"tool_pocket_capacity": 1001,
|
||||
"fixed_tick": True,
|
||||
"hal_pins": ["spindle.0.*", "spindle.1.*", "iocontrol.0.tool-*", "motion.probe-input"],
|
||||
"scheduler": "sim-io-servo",
|
||||
},
|
||||
"pipeline": {
|
||||
"spindle": ["command", "inhibit_or_fault_gate", "acceleration_limit", "at_speed_and_brake", "revs_and_index"],
|
||||
"tool": ["prepare", "prepared_ack", "spindle_stopped_gate", "change", "changed_ack", "abort_or_fault"],
|
||||
"probe": ["arm", "input_edge", "forward_pose_latch", "trip_or_disarm"],
|
||||
"kinematics_association": "probe latch consumes the motion feedback pose; tool change requires stopped spindle; estop cancels motion-facing IO",
|
||||
},
|
||||
"boundaries": {
|
||||
"invalid_pin_type": native["pin"]["invalid_type"],
|
||||
"spindle_limit": native["spindle"]["limit"],
|
||||
"tool_change_while_spinning": native["spindle"]["tool_busy"],
|
||||
"probe_trigger": native["probe"]["status"],
|
||||
"probe_state": native["probe"]["state"],
|
||||
"estop_command": native["estop"]["command"],
|
||||
"estop_recovery_before_clear": native["estop"]["recover_before_clear"],
|
||||
"recovery": native["estop"]["recover"],
|
||||
"fault_clear_while_asserted": native["tool"]["fault_clear"],
|
||||
"state_hash_stable": native["state_hash"] == native["repeat_hash"],
|
||||
},
|
||||
"samples": {
|
||||
"native": native,
|
||||
"wasm": wasm,
|
||||
"byte_identical_json": canonical(native) == canonical(wasm),
|
||||
"state_hash_identical": native["state_hash"] == wasm["state_hash"] and native["repeat_hash"] == wasm["repeat_hash"],
|
||||
},
|
||||
"native_wasm": {
|
||||
"status": "passed",
|
||||
"native_command": "build-native/sim_io_test",
|
||||
"wasm_command": "node build-wasm/sim_io_wasm_test.js",
|
||||
},
|
||||
"oracle": {
|
||||
"status": "blocked",
|
||||
"reason": "locked LinuxCNC source has no linuxcnc/unit_tests/kinematics oracle root and no browser IO oracle",
|
||||
"blocked_samples": ["spindle acceleration/at-speed", "spindle orient", "tool prepare/change", "tool abort/fault/recovery", "probe pose latch", "estop and recovery"],
|
||||
},
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical(report).encode()).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/io-runtime.report.json"))
|
||||
args = parser.parse_args()
|
||||
report = generate(args.repo_root, args.repo_root / args.output)
|
||||
print(f"io_runtime={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
71
scripts/generate_kinematics_feedback_report.py
Normal file
71
scripts/generate_kinematics_feedback_report.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate deterministic evidence for FK feedback integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
INPUTS = {
|
||||
"feedback_header": "wasm-core/include/sim_kinematics_feedback.h",
|
||||
"feedback_source": "wasm-core/src/sim_kinematics_feedback.c",
|
||||
"kinematics_header": "wasm-core/include/sim_kinematics.h",
|
||||
"kinematics_source": "wasm-core/src/sim_kinematics.c",
|
||||
"test": "wasm-core/tests/sim_kinematics_feedback_test.c",
|
||||
"linuxcnc_control": "linuxcnc/src/emc/motion/control.c",
|
||||
"linuxcnc_motion": "linuxcnc/src/emc/motion/motion.c",
|
||||
}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def run_json(command: list[str], root: Path) -> dict[str, object]:
|
||||
result = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"{command[0]} exited {result.returncode}: {result.stderr.strip()}")
|
||||
for line in result.stdout.splitlines():
|
||||
if line.lstrip().startswith("{"):
|
||||
item = json.loads(line)
|
||||
if item.get("schema") == "kinematics-feedback-samples.v1":
|
||||
return item
|
||||
raise RuntimeError("runner did not emit kinematics-feedback-samples.v1")
|
||||
|
||||
|
||||
def generate(root: Path, output: Path) -> dict[str, object]:
|
||||
root = root.resolve()
|
||||
native = run_json([str(root / "build-native/sim_kinematics_feedback_test")], root)
|
||||
wasm = run_json(["node", str(root / "build-wasm/sim_kinematics_feedback_wasm_test.js")], root)
|
||||
report = {
|
||||
"schema": "kinematics-feedback-report.v1",
|
||||
"status": "partial",
|
||||
"implementation_status": "verified",
|
||||
"source_records": {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()},
|
||||
"pipeline": {"order": ["actual_joint_feedback", "forward_kinematics", "pose_flags_commit"], "last_valid_pose_policy": "retain on error", "state_fields": ["pose", "joints", "forward_flags", "inverse_flags", "tick", "valid", "last_error", "kinematics_error"]},
|
||||
"configuration_candidates": [{"id": "trivkins-xyz-feedback", "kind": "derived", "path": INPUTS["test"], "coordinates": "XYZ"}, {"id": "inverse-only-feedback-rejection", "kind": "derived", "path": INPUTS["test"], "coordinates": "XYZBC", "purpose": "forward feedback rejects unsupported mode without publishing a pose"}],
|
||||
"boundaries": {"stale_read": native["stale_read"], "nonfinite": native["nonfinite"], "joint_count": native["count_error"], "forward_failure": native["singular"]["status"], "forward_failure_kinematics": native["singular"]["kinematics_error"], "failed_feedback_valid": native["singular"]["valid"], "state_hash_stable": native["nominal"]["state_hash"] == native["nominal"]["repeat_hash"]},
|
||||
"samples": {"native": native, "wasm": wasm, "byte_identical_json": json.dumps(native, sort_keys=True, separators=(",", ":")) == json.dumps(wasm, sort_keys=True, separators=(",", ":")), "state_hash_identical": native["nominal"]["state_hash"] == wasm["nominal"]["state_hash"]},
|
||||
"native_wasm": {"status": "passed", "native_command": "build-native/sim_kinematics_feedback_test", "wasm_command": "node build-wasm/sim_kinematics_feedback_wasm_test.js"},
|
||||
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source", "blocked_samples": ["nominal FK feedback", "stale read", "nonfinite feedback", "joint count mismatch", "unsupported forward", "state hash"]},
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(json.dumps(report, sort_keys=True, separators=(",", ":")).encode()).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-feedback.report.json"))
|
||||
args = parser.parse_args()
|
||||
report = generate(args.repo_root, args.repo_root / args.output)
|
||||
print(f"kinematics_feedback={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
71
scripts/generate_kinematics_home_limit_report.py
Normal file
71
scripts/generate_kinematics_home_limit_report.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate deterministic evidence for home and limit integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
INPUTS = {
|
||||
"home_header": "wasm-core/include/sim_kinematics_home_limit.h",
|
||||
"home_source": "wasm-core/src/sim_kinematics_home_limit.c",
|
||||
"kinematics_header": "wasm-core/include/sim_kinematics.h",
|
||||
"kinematics_source": "wasm-core/src/sim_kinematics.c",
|
||||
"test": "wasm-core/tests/sim_kinematics_home_limit_test.c",
|
||||
"linuxcnc_homing": "linuxcnc/src/emc/motion/homing.c",
|
||||
"linuxcnc_homemod": "linuxcnc/src/emc/motion/homemod.c",
|
||||
}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def run_json(command: list[str], root: Path) -> dict[str, object]:
|
||||
result = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"{command[0]} exited {result.returncode}: {result.stderr.strip()}")
|
||||
for line in result.stdout.splitlines():
|
||||
if line.lstrip().startswith("{"):
|
||||
item = json.loads(line)
|
||||
if item.get("schema") == "kinematics-home-limit-samples.v1":
|
||||
return item
|
||||
raise RuntimeError("runner did not emit kinematics-home-limit-samples.v1")
|
||||
|
||||
|
||||
def generate(root: Path, output: Path) -> dict[str, object]:
|
||||
root = root.resolve()
|
||||
native = run_json([str(root / "build-native/sim_kinematics_home_limit_test")], root)
|
||||
wasm = run_json(["node", str(root / "build-wasm/sim_kinematics_home_limit_wasm_test.js")], root)
|
||||
report = {
|
||||
"schema": "kinematics-home-limit-report.v1",
|
||||
"status": "partial",
|
||||
"implementation_status": "verified",
|
||||
"source_records": {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()},
|
||||
"pipeline": {"order": ["home_sequence", "kinematics_home", "limit_check", "forward_feedback"], "state_fields": ["homed", "home_sequence", "tick", "pose", "joints", "last_error", "limit_error", "kinematics_error"], "failed_limit_policy": "retain last valid pose and joints"},
|
||||
"configuration_candidates": [{"id": "trivkins-xyz-home-limits", "kind": "derived", "path": INPUTS["test"], "coordinates": "XYZ", "limits": "[-5,5] per axis"}],
|
||||
"boundaries": {"unhomed": native["before_home"], "sequence": native["sequence_error"], "home": native["home_status"], "limit": native["limit_status"], "nonfinite": native["nonfinite_status"], "joint_count": native["count_status"], "state_hash_stable": native["state"]["hash"] == native["state"]["repeat_hash"], "retains_valid_pose": native["state"]["pose"] == [1.0, 2.0, 3.0]},
|
||||
"samples": {"native": native, "wasm": wasm, "byte_identical_json": json.dumps(native, sort_keys=True, separators=(",", ":")) == json.dumps(wasm, sort_keys=True, separators=(",", ":")), "state_hash_identical": native["state"]["hash"] == wasm["state"]["hash"]},
|
||||
"native_wasm": {"status": "passed", "native_command": "build-native/sim_kinematics_home_limit_test", "wasm_command": "node build-wasm/sim_kinematics_home_limit_wasm_test.js"},
|
||||
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source", "blocked_samples": ["unhomed check", "home sequence", "kinematics home", "joint limit", "nonfinite joint", "joint count", "state hash"]},
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(json.dumps(report, sort_keys=True, separators=(",", ":")).encode()).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-home-limit.report.json"))
|
||||
args = parser.parse_args()
|
||||
report = generate(args.repo_root, args.repo_root / args.output)
|
||||
print(f"kinematics_home_limit={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
73
scripts/generate_kinematics_motion_report.py
Normal file
73
scripts/generate_kinematics_motion_report.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate deterministic evidence for TP -> IK -> motion integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
INPUTS = {
|
||||
"motion_header": "wasm-core/include/sim_motion_queue.h",
|
||||
"motion_source": "wasm-core/src/sim_motion_queue.c",
|
||||
"kinematics_motion_header": "wasm-core/include/sim_kinematics_motion.h",
|
||||
"kinematics_motion_source": "wasm-core/src/sim_kinematics_motion.c",
|
||||
"kinematics_header": "wasm-core/include/sim_kinematics.h",
|
||||
"kinematics_source": "wasm-core/src/sim_kinematics.c",
|
||||
"test": "wasm-core/tests/sim_kinematics_motion_test.c",
|
||||
"linuxcnc_control": "linuxcnc/src/emc/motion/control.c",
|
||||
"linuxcnc_motion": "linuxcnc/src/emc/motion/motion.c",
|
||||
}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def run_json(command: list[str], root: Path) -> dict[str, object]:
|
||||
result = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"{command[0]} exited {result.returncode}: {result.stderr.strip()}")
|
||||
for line in result.stdout.splitlines():
|
||||
if line.lstrip().startswith("{"):
|
||||
item = json.loads(line)
|
||||
if item.get("schema") == "kinematics-motion-samples.v1":
|
||||
return item
|
||||
raise RuntimeError("runner did not emit kinematics-motion-samples.v1")
|
||||
|
||||
|
||||
def generate(root: Path, output: Path) -> dict[str, object]:
|
||||
root = root.resolve()
|
||||
native = run_json([str(root / "build-native/sim_kinematics_motion_test")], root)
|
||||
wasm = run_json(["node", str(root / "build-wasm/sim_kinematics_motion_wasm_test.js")], root)
|
||||
report = {
|
||||
"schema": "kinematics-motion-report.v1",
|
||||
"status": "partial",
|
||||
"implementation_status": "verified",
|
||||
"source_records": {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()},
|
||||
"pipeline": {"order": ["fixed_tick", "trajectory_planner", "kinematics_inverse", "atomic_joint_queue"], "joint_queue_capacity": 64, "planner_queue_capacity": 32, "transaction": "snapshot planner and kinematics; rollback on IK, planner, nonfinite, or capacity failure"},
|
||||
"configuration_candidates": [{"id": "trivkins-xyz-fixed-tick", "kind": "derived", "path": INPUTS["test"], "coordinates": "XYZ", "purpose": "deterministic TP to inverse to joint sample"}, {"id": "xyzbc-singular-rollback", "kind": "derived", "path": INPUTS["test"], "coordinates": "XYZBC", "purpose": "B=90 inverse failure leaves planner untouched"}],
|
||||
"boundaries": {"pause": native["nominal"]["pause_status"], "capacity": native["capacity"]["status"], "inverse_failure": native["inverse_failure"]["status"], "inverse_error": native["inverse_failure"]["kinematics_error"], "nonfinite": native["nonfinite_status"], "capacity_planner_rollback": native["capacity"]["planner_before"] == native["capacity"]["planner_after"], "inverse_planner_rollback": native["inverse_failure"]["planner_before"] == native["inverse_failure"]["planner_after"], "state_hash_stable": native["nominal"]["state_hash"] == native["nominal"]["repeat_hash"]},
|
||||
"samples": {"native": native, "wasm": wasm, "byte_identical_json": json.dumps(native, sort_keys=True, separators=(",", ":")) == json.dumps(wasm, sort_keys=True, separators=(",", ":")), "state_hash_identical": native["nominal"]["state_hash"] == wasm["nominal"]["state_hash"] and native["capacity"]["state_hash"] == wasm["capacity"]["state_hash"]},
|
||||
"native_wasm": {"status": "passed", "native_command": "build-native/sim_kinematics_motion_test", "wasm_command": "node build-wasm/sim_kinematics_motion_wasm_test.js"},
|
||||
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source", "blocked_samples": ["TP to IK fixed tick", "joint queue capacity", "pause/resume", "singular IK rollback", "nonfinite segment"]},
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(json.dumps(report, sort_keys=True, separators=(",", ":")).encode()).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-motion.report.json"))
|
||||
args = parser.parse_args()
|
||||
report = generate(args.repo_root, args.repo_root / args.output)
|
||||
print(f"kinematics_motion={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
90
scripts/generate_kinematics_registry_report.py
Normal file
90
scripts/generate_kinematics_registry_report.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate deterministic evidence for the static kinematics registry."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
INPUTS = {
|
||||
"linuxcnc_header": "linuxcnc/src/emc/kinematics/kinematics.h",
|
||||
"sim_header": "wasm-core/include/sim_kinematics.h",
|
||||
"implementation": "wasm-core/src/sim_kinematics_registry.c",
|
||||
"kinematics_implementation": "wasm-core/src/sim_kinematics.c",
|
||||
"test": "wasm-core/tests/sim_kinematics_registry_test.c",
|
||||
"config_xyzac": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"config_genser": "linuxcnc/configs/sim/axis/vismach/puma/puma560.ini",
|
||||
"config_pentapod": "linuxcnc/configs/sim/axis/pentapod/pentapod.ini",
|
||||
}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def run_json(command: list[str], root: Path) -> dict[str, object]:
|
||||
result = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"{command[0]} exited {result.returncode}: {result.stderr.strip()}")
|
||||
for line in result.stdout.splitlines():
|
||||
if line.lstrip().startswith("{"):
|
||||
item = json.loads(line)
|
||||
if item.get("schema") == "kinematics-registry-samples.v1":
|
||||
return item
|
||||
raise RuntimeError("runner did not emit kinematics-registry-samples.v1")
|
||||
|
||||
|
||||
def generate(root: Path, output: Path) -> dict[str, object]:
|
||||
root = root.resolve()
|
||||
native = run_json([str(root / "build-native/sim_kinematics_registry_test")], root)
|
||||
wasm = run_json(["node", str(root / "build-wasm/sim_kinematics_registry_wasm_test.js")], root)
|
||||
report = {
|
||||
"schema": "kinematics-registry-report.v1",
|
||||
"status": "partial",
|
||||
"implementation_status": "verified",
|
||||
"source_records": {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()},
|
||||
"configuration_candidates": [
|
||||
{"id": "xyzac-trt", "kind": "real", "path": INPUTS["config_xyzac"], "type": 2, "coordinates": "XYZAC", "parameters": ["offsets", "tool", "limits"]},
|
||||
{"id": "genser-puma560", "kind": "real", "path": INPUTS["config_genser"], "type": 13, "coordinates": "XYZABC", "parameters": ["DH", "solver", "limits"]},
|
||||
{"id": "pentapod", "kind": "real", "path": INPUTS["config_pentapod"], "type": 18, "coordinates": "XYZAB", "parameters": ["geometry", "tool", "solver", "limits"]},
|
||||
],
|
||||
"registry": {
|
||||
"abi_version": 1,
|
||||
"entry_count": native["count"],
|
||||
"verified_entries": native["verified"],
|
||||
"capability_union": native["capability_union"],
|
||||
"parameter_union": native["parameter_union"],
|
||||
"state_hash": native["registry_hash"],
|
||||
"capabilities": {"forward": 1, "inverse": 2, "identity": 4, "iterative": 8, "switchable": 16, "extra_joints": 32, "duplicate_coordinates": 64},
|
||||
"parameters": {"mapping": 1, "limits": 2, "offsets": 4, "tool": 8, "pivot": 16, "geometry": 32, "DH": 64, "solver": 128},
|
||||
},
|
||||
"validation": {"lookup_status": native["lookup_status"], "iterative_genser_and_DH_status": native["iterative_require"], "type_mismatch": native["type_mismatch"], "coordinate_error": native["coordinate_error"], "joint_error": native["joint_error"], "unknown_name": native["unknown_name"], "path_name": native["path_name"], "range_error": native["range_error"], "capability_error": native["capability_error"], "parameter_error": native["parameter_error"]},
|
||||
"samples": {
|
||||
"native": native,
|
||||
"wasm": wasm,
|
||||
"byte_identical_json": json.dumps(native, sort_keys=True, separators=(",", ":")) == json.dumps(wasm, sort_keys=True, separators=(",", ":")),
|
||||
"state_hash_identical": native["registry_hash"] == wasm["registry_hash"],
|
||||
},
|
||||
"native_wasm": {"status": "passed", "native_command": "build-native/sim_kinematics_registry_test", "wasm_command": "node build-wasm/sim_kinematics_registry_wasm_test.js"},
|
||||
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source", "blocked_samples": ["18-entry registry", "capability requirements", "parameter masks", "real config validation", "unknown/path names"]},
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(json.dumps(report, sort_keys=True, separators=(",", ":")).encode()).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-registry.report.json"))
|
||||
args = parser.parse_args()
|
||||
report = generate(args.repo_root, args.repo_root / args.output)
|
||||
print(f"kinematics_registry={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -43,14 +43,19 @@ def generate(root: Path, output: Path) -> dict[str, object]:
|
||||
wasm_all = run_json(["node", str(root / "build-wasm/sim_kinematics_wasm_test.js")], root)
|
||||
native = {"pentapod": native_all["pentapod"], "boundaries": native_all["delta_edges"]["pentapod"]}
|
||||
wasm = {"pentapod": wasm_all["pentapod"], "boundaries": wasm_all["delta_edges"]["pentapod"]}
|
||||
config_text = (root / INPUTS["config"]).read_text(encoding="utf-8")
|
||||
report = {
|
||||
"schema": "pentapod-kinematics-report.v1", "status": "partial", "implementation_status": "verified",
|
||||
"source_records": {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()},
|
||||
"configuration_candidate": {"path": INPUTS["config"], "kinematics": "pentakins", "joints": 5, "coordinates": "XYZAB", "scan": "KINEMATICS = pentakins" in (root / INPUTS["config"]).read_text(encoding="utf-8"), "hal_geometry": parse_hal(root / INPUTS["hal"]), "hal_pins": ["last-iterations", "max-iterations", "limit-iterations", "max-error", "convergence-criterion", "tool-offset"]},
|
||||
"geometry": {"struts": 5, "orientation": "roll/pitch only; yaw fixed at zero", "units": "configuration units; XYZ linear and AB degrees"},
|
||||
"configuration_candidate": {"path": INPUTS["config"], "kinematics": "pentakins", "joints": 5, "coordinates": "XYZAB", "scan": "KINEMATICS = pentakins" in config_text and "JOINTS = 5" in config_text, "provenance": "shipped LinuxCNC simulation", "hal_geometry": parse_hal(root / INPUTS["hal"]), "hal_pins": ["last-iterations", "max-iterations", "limit-iterations", "max-error", "convergence-criterion", "tool-offset"]},
|
||||
"configuration_candidates": [
|
||||
{"id": "shipped-pentapod", "path": INPUTS["config"], "kind": "real", "coordinates": "XYZAB", "joints": 5},
|
||||
{"id": "offset-seed-regression", "path": "wasm-core/tests/sim_kinematics_test.c", "kind": "derived", "coordinates": "XYZAB", "joints": 5, "initial_pose": [0.0, 500.0, 300.0, 0.0, 0.0], "target_pose": [0.0, 500.0, 300.0, 0.01, -0.01], "purpose": "non-zero roll/pitch Newton convergence and native/WASM comparison"},
|
||||
],
|
||||
"geometry": {"struts": 5, "orientation": "roll/pitch only; yaw is constrained to zero in the forward result", "units": "configuration units; XYZ linear and AB degrees", "tool_offset": "applied to both base and effector HAL z coordinates"},
|
||||
"mapping": {"coordinates": "XYZAB", "kinematics_joint_count": 5, "joint_semantics": ["strut-0", "strut-1", "strut-2", "strut-3", "strut-4"], "extra_joint_policy": "rejected"},
|
||||
"solver": {"inverse": "closed-form radial/z strut lengths", "forward": "Newton-Raphson 5x5 numerical Jacobian", "seed": [0.0, 500.0, 300.0, 0.0, 0.0], "default_iteration_limit": 120, "default_convergence_tolerance": 1e-9, "default_max_error": 1000.0, "alternate_seed_flag": "SIM_KIN_GENHEX_ALTERNATE_SEED bit 1; negative-z seed is explicitly bounded"},
|
||||
"workspace_and_boundaries": {"nominal": native["pentapod"], "alternate_seed": native["boundaries"]["alternate_pose"], "alternate_seed_error": native["boundaries"]["alternate_status"], "nonpositive_joint_error": native["boundaries"]["nonpositive"], "iteration_limit_error": native["boundaries"]["iteration_limit"], "max_error_boundary": native["boundaries"]["max_error"], "singular_jacobian_error": native["boundaries"]["singular"], "joint_limit_error": native["boundaries"]["limit_error"], "iterations": native["pentapod"]["iterations"], "residual": native["pentapod"]["residual"]},
|
||||
"solver": {"inverse": "closed-form radial/z strut lengths", "forward": "Newton-Raphson 5x5 numerical Jacobian", "seed": [0.0, 500.0, 300.0, 0.0, 0.0], "seed_units": "XYZ in configuration units; A/B in radians internally", "default_iteration_limit": 120, "default_convergence_tolerance": 1e-9, "default_max_error": 1000.0, "alternate_seed_flag": "SIM_KIN_GENHEX_ALTERNATE_SEED bit 1; negative-z seed is explicitly bounded"},
|
||||
"workspace_and_boundaries": {"nominal": native["pentapod"], "alternate_seed": native["boundaries"]["alternate_pose"], "alternate_seed_error": native["boundaries"]["alternate_status"], "alternate_seed_iterations": native["boundaries"].get("alternate_iterations", 0), "nonpositive_joint_error": native["boundaries"]["nonpositive"], "nonpositive_joint_iterations": native["boundaries"].get("nonpositive_iterations", 0), "iteration_limit_error": native["boundaries"]["iteration_limit"], "iteration_limit_iterations": native["boundaries"].get("iteration_limit_iterations", 0), "max_error_boundary": native["boundaries"]["max_error"], "max_error_iterations": native["boundaries"].get("max_error_iterations", 0), "singular_jacobian_error": native["boundaries"]["singular"], "singular_jacobian_iterations": native["boundaries"].get("singular_iterations", 0), "joint_limit_error": native["boundaries"]["limit_error"], "posture_constraint": "C/yaw is not a configured joint and forward output is exactly zero", "iterations": native["pentapod"]["iterations"], "residual": native["pentapod"]["residual"]},
|
||||
"samples": {"native": native, "wasm": wasm, "byte_identical_json": json.dumps(native, sort_keys=True, separators=(",", ":")) == json.dumps(wasm, sort_keys=True, separators=(",", ":")), "state_hash_identical": native["pentapod"]["state_hash"] == wasm["pentapod"]["state_hash"]},
|
||||
"native_wasm": {"status": "passed", "native_command": "build-native/sim_kinematics_test", "wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"},
|
||||
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source", "blocked_samples": ["nominal five-strut pose", "negative-z seed", "nonpositive strut", "iteration limit", "maximum error", "singular Jacobian", "joint limit"]},
|
||||
|
||||
@@ -121,7 +121,48 @@ def make_manifest_entry(source: dict[str, object], repo_root: Path | None = None
|
||||
path = repo_root / source_path
|
||||
if path.is_file():
|
||||
is_kinematics_config = bool(KINEMATICS_CONFIG_RE.search(path.read_text(encoding="utf-8", errors="strict")))
|
||||
is_kinematics_abi = source_path in {"wasm-core/include/sim_kinematics.h", "wasm-core/src/sim_kinematics.c", "wasm-core/tests/sim_kinematics_test.c", "wasm-core/tests/sim_kinematics_test_main.c"}
|
||||
if source_path == "linuxcnc/configs/sim/axis/pentapod/kinematics.hal":
|
||||
is_kinematics_config = True
|
||||
is_kinematics_abi = source_path in {"wasm-core/include/sim_kinematics.h", "wasm-core/src/sim_kinematics.c", "wasm-core/src/sim_switchkins.c", "wasm-core/src/sim_userkins.c", "wasm-core/src/sim_kinematics_registry.c", "wasm-core/tests/sim_kinematics_test.c", "wasm-core/tests/sim_kinematics_test_main.c", "wasm-core/tests/sim_switchkins_test.c", "wasm-core/tests/sim_userkins_test.c", "wasm-core/tests/sim_kinematics_registry_test.c"}
|
||||
is_switchkins_source = source_path in {
|
||||
"linuxcnc/src/emc/kinematics/switchkins.c", "linuxcnc/src/emc/kinematics/switchkins.h",
|
||||
"linuxcnc/src/emc/kinematics/xyzac-trt-kins.c",
|
||||
"wasm-core/src/sim_switchkins.c",
|
||||
}
|
||||
is_switchkins_config = source_path in {
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
}
|
||||
is_userkins_source = source_path in {
|
||||
"linuxcnc/src/emc/kinematics/userkfuncs.c",
|
||||
"linuxcnc/src/emc/kinematics/ugenserkins.c",
|
||||
}
|
||||
is_userkins_config = source_path in {
|
||||
"linuxcnc/configs/sim/axis/vismach/scara/scara.ini",
|
||||
"linuxcnc/configs/sim/axis/vismach/puma/puma560.ini",
|
||||
}
|
||||
is_userkins_implementation = source_path in {
|
||||
"wasm-core/src/sim_userkins.c", "wasm-core/tests/sim_userkins_test.c",
|
||||
}
|
||||
is_registry_implementation = source_path in {
|
||||
"wasm-core/src/sim_kinematics_registry.c", "wasm-core/tests/sim_kinematics_registry_test.c",
|
||||
}
|
||||
is_motion_implementation = source_path in {
|
||||
"wasm-core/include/sim_kinematics_motion.h", "wasm-core/src/sim_kinematics_motion.c", "wasm-core/tests/sim_kinematics_motion_test.c",
|
||||
}
|
||||
is_feedback_implementation = source_path in {
|
||||
"wasm-core/include/sim_kinematics_feedback.h", "wasm-core/src/sim_kinematics_feedback.c", "wasm-core/tests/sim_kinematics_feedback_test.c",
|
||||
}
|
||||
is_home_limit_implementation = source_path in {
|
||||
"wasm-core/include/sim_kinematics_home_limit.h", "wasm-core/src/sim_kinematics_home_limit.c", "wasm-core/tests/sim_kinematics_home_limit_test.c",
|
||||
}
|
||||
is_io_implementation = source_path in {
|
||||
"wasm-core/include/sim_io.h", "wasm-core/src/sim_io.c", "wasm-core/tests/sim_io_test.c",
|
||||
}
|
||||
is_io_source = source_path in {
|
||||
"linuxcnc/src/emc/iotask/ioControl.cc", "linuxcnc/src/emc/iotask/ioControl_v2.cc",
|
||||
"linuxcnc/src/hal/components/sim_spindle.comp",
|
||||
}
|
||||
is_delta_source = source_path.startswith("linuxcnc/src/emc/kinematics/lineardelta") or source_path.startswith("linuxcnc/src/emc/kinematics/rotarydelta")
|
||||
is_delta_config = is_kinematics_config and any(token in source_path.lower() for token in ("ldelta", "rdelta"))
|
||||
is_delta_implementation = is_kinematics_abi and source_path in {"wasm-core/include/sim_kinematics.h", "wasm-core/src/sim_kinematics.c", "wasm-core/tests/sim_kinematics_test.c", "wasm-core/tests/sim_kinematics_test_main.c"}
|
||||
@@ -130,7 +171,15 @@ def make_manifest_entry(source: dict[str, object], repo_root: Path | None = None
|
||||
is_genhex_source = source_path in {"linuxcnc/src/emc/kinematics/genhexkins.c", "linuxcnc/src/emc/kinematics/genhexkins.h"}
|
||||
is_genhex_config = is_kinematics_config and source_path == "linuxcnc/configs/sim/axis/vismach/hexapod-sim/hexapod.ini"
|
||||
is_genhex_hal = source_path == "linuxcnc/configs/sim/axis/vismach/hexapod-sim/kinematics.hal"
|
||||
is_genhex_implementation = source_path in {"wasm-core/include/sim_kinematics.h", "wasm-core/src/sim_kinematics.c", "wasm-core/tests/sim_kinematics_test.c", "wasm-core/tests/sim_kinematics_test_main.c"}
|
||||
is_penta_source = source_path in {"linuxcnc/src/emc/kinematics/pentakins.c", "linuxcnc/src/emc/kinematics/pentakins.h"}
|
||||
is_penta_config = is_kinematics_config and source_path == "linuxcnc/configs/sim/axis/pentapod/pentapod.ini"
|
||||
is_penta_hal = source_path == "linuxcnc/configs/sim/axis/pentapod/kinematics.hal"
|
||||
is_penta_implementation = is_kinematics_abi
|
||||
is_recovery_implementation = source_path in {
|
||||
"wasm-core/include/sim_event.h", "wasm-core/src/sim_event.c", "wasm-core/tests/sim_event_test.c",
|
||||
"web/worker/replay.mjs", "web/worker/sim-session.mjs", "web/worker/test_replay.mjs",
|
||||
"web/workspace/journal.mjs", "web/workspace/recovery.mjs", "web/workspace/test_recovery.mjs",
|
||||
}
|
||||
is_corexy_hal = source_path in {
|
||||
"linuxcnc/src/hal/components/corexy_by_hal.comp",
|
||||
"linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini",
|
||||
@@ -141,20 +190,79 @@ def make_manifest_entry(source: dict[str, object], repo_root: Path | None = None
|
||||
"wasm-core/tests/sim_hal_test.c",
|
||||
"wasm-core/tests/sim_hal_test_main.c",
|
||||
}
|
||||
if is_kinematics_source or is_kinematics_config or is_kinematics_abi or is_corexy_hal:
|
||||
is_sim_configuration = source_path.startswith("linuxcnc/configs/sim/")
|
||||
if is_recovery_implementation:
|
||||
entry.update({
|
||||
"domain": "recovery",
|
||||
"feature_id": "feature:recovery-k7-replay",
|
||||
"dependencies": ["coverage/workspace-recovery.report.json", "coverage/io-runtime.report.json", "coverage/kinematics-motion.report.json", "coverage/kinematics-feedback.report.json"],
|
||||
"outputs": ["coverage/workspace-recovery.report.json"],
|
||||
"errors": ["blocked:official recovery oracle is absent from locked source"],
|
||||
"test_id": "audit:K7-RECOVERY-01",
|
||||
"oracle": {"status": "blocked", "report": "coverage/workspace-recovery.report.json"},
|
||||
"mapping_status": "verified_partial",
|
||||
})
|
||||
if is_io_implementation or is_io_source:
|
||||
entry.update({
|
||||
"domain": "io",
|
||||
"feature_id": "feature:io-k6-spindle-tool-probe",
|
||||
"dependencies": ["coverage/io-runtime.report.json", "coverage/hal-runtime.report.json", "coverage/kinematics-feedback.report.json"],
|
||||
"outputs": ["coverage/io-runtime.report.json"],
|
||||
"errors": ["blocked:official IO/kinematics oracle is absent from locked source"],
|
||||
"test_id": "audit:K6-IO-01",
|
||||
"oracle": {"status": "blocked", "report": "coverage/io-runtime.report.json"},
|
||||
"mapping_status": "verified_partial",
|
||||
})
|
||||
if is_kinematics_source or is_kinematics_config or is_kinematics_abi or is_corexy_hal or is_switchkins_source or is_switchkins_config or is_userkins_source or is_userkins_config or is_userkins_implementation or is_motion_implementation or is_feedback_implementation or is_home_limit_implementation:
|
||||
# K0-INV-01 audits are source-closure evidence, not an implementation
|
||||
# claim. Keep the seed planned while linking every kinematics source
|
||||
# to the deterministic report and its explicit blocked oracle.
|
||||
entry.update({
|
||||
"domain": "hal" if is_corexy_hal else "kinematics",
|
||||
"feature_id": "feature:kinematics-k2-hal" if is_corexy_hal else "feature:kinematics-k4-tripod" if (is_tripod_source or is_tripod_config) else "feature:kinematics-k4-genhex" if (is_genhex_source or is_genhex_config or is_genhex_hal or is_genhex_implementation) else "feature:kinematics-k4-delta" if (is_delta_source or is_delta_config or is_delta_implementation) else "feature:kinematics-k0" if not is_kinematics_abi else "feature:kinematics-k1-contract",
|
||||
"dependencies": ["coverage/coupled-kinematics.report.json", "coverage/config-resources.jsonl"] if is_corexy_hal else ["coverage/tripod-kinematics.report.json", "coverage/delta-kinematics.report.json"] if (is_tripod_source or is_tripod_config) else ["coverage/genhex-kinematics.report.json", "coverage/kinematics-source-closure.report.json"] if (is_genhex_source or is_genhex_config or is_genhex_hal or is_genhex_implementation) else ["coverage/delta-kinematics.report.json", "coverage/kinematics-source-closure.report.json"] if (is_delta_source or is_delta_config or is_delta_implementation) else ["coverage/source-index.jsonl", "coverage/source-symbols.jsonl"] if is_kinematics_source else ["coverage/kinematics-source-closure.report.json", "coverage/config-resources.jsonl"] if is_kinematics_config else ["wasm-core/include/sim_kinematics.h", "wasm-core/CMakeLists.txt"],
|
||||
"outputs": ["coverage/corexy-hal.report.json"] if is_corexy_hal else ["coverage/tripod-kinematics.report.json"] if (is_tripod_source or is_tripod_config) else ["coverage/genhex-kinematics.report.json"] if (is_genhex_source or is_genhex_config or is_genhex_hal or is_genhex_implementation) else ["coverage/delta-kinematics.report.json"] if (is_delta_source or is_delta_config or is_delta_implementation) else ["coverage/kinematics-source-closure.report.json", "coverage/trivkins-integration.report.json", "coverage/coupled-kinematics.report.json", "coverage/fiveaxis-kinematics.report.json", "coverage/trt-kinematics.report.json", "coverage/robot-kinematics.report.json", "coverage/genser-kinematics.report.json"] if is_kinematics_source else ["coverage/kinematics-config-graph.report.json", "coverage/kinematics-oracle-samples.jsonl", "coverage/kinematics-oracle.report.json", "coverage/kinematics-golden-schema.json", "coverage/coupled-kinematics.report.json", "coverage/fiveaxis-kinematics.report.json", "coverage/trt-kinematics.report.json", "coverage/robot-kinematics.report.json", "coverage/genser-kinematics.report.json"] if is_kinematics_config else ["coverage/kinematics-abi-contract.json", "coverage/kinematics-mapping-contract.json", "coverage/kinematics-numeric-policy.json", "coverage/coupled-kinematics.report.json", "coverage/fiveaxis-kinematics.report.json", "coverage/trt-kinematics.report.json", "coverage/robot-kinematics.report.json", "coverage/genser-kinematics.report.json"],
|
||||
"feature_id": "feature:kinematics-k6-home" if is_home_limit_implementation else "feature:kinematics-k6-feedback" if is_feedback_implementation else "feature:kinematics-k6-motion" if is_motion_implementation else "feature:kinematics-k5-cfg" if is_registry_implementation else "feature:kinematics-k5-userkins" if (is_userkins_source or is_userkins_config or is_userkins_implementation) else "feature:kinematics-k5-switchkins" if (is_switchkins_source or is_switchkins_config) else "feature:kinematics-k2-hal" if is_corexy_hal else "feature:kinematics-k4-tripod" if (is_tripod_source or is_tripod_config) else "feature:kinematics-k4-genhex" if (is_genhex_source or is_genhex_config or is_genhex_hal) else "feature:kinematics-k4-pentapod" if (is_penta_source or is_penta_config or is_penta_hal or is_penta_implementation) else "feature:kinematics-k4-delta" if (is_delta_source or is_delta_config or is_delta_implementation) else "feature:kinematics-k0" if not is_kinematics_abi else "feature:kinematics-k1-contract",
|
||||
"dependencies": ["coverage/kinematics-home-limit.report.json", "coverage/kinematics-feedback.report.json", "coverage/kinematics-registry.report.json"] if is_home_limit_implementation else ["coverage/kinematics-feedback.report.json", "coverage/kinematics-motion.report.json", "coverage/kinematics-registry.report.json"] if is_feedback_implementation else ["coverage/kinematics-motion.report.json", "coverage/motion-runtime.report.json", "coverage/kinematics-registry.report.json"] if is_motion_implementation else ["coverage/kinematics-registry.report.json", "coverage/kinematics-abi-contract.json", "coverage/kinematics-numeric-policy.json"] if is_registry_implementation else ["coverage/userkins-kinematics.report.json", "coverage/genser-kinematics.report.json"] if (is_userkins_source or is_userkins_config or is_userkins_implementation) else ["coverage/switchkins-kinematics.report.json", "coverage/kinematics-source-closure.report.json"] if (is_switchkins_source or is_switchkins_config) else ["coverage/coupled-kinematics.report.json", "coverage/config-resources.jsonl"] if is_corexy_hal else ["coverage/tripod-kinematics.report.json", "coverage/delta-kinematics.report.json"] if (is_tripod_source or is_tripod_config) else ["coverage/genhex-kinematics.report.json", "coverage/kinematics-source-closure.report.json"] if (is_genhex_source or is_genhex_config or is_genhex_hal) else ["coverage/pentapod-kinematics.report.json", "coverage/kinematics-source-closure.report.json"] if (is_penta_source or is_penta_config or is_penta_hal or is_penta_implementation) else ["coverage/delta-kinematics.report.json", "coverage/kinematics-source-closure.report.json"] if (is_delta_source or is_delta_config or is_delta_implementation) else ["coverage/source-index.jsonl", "coverage/source-symbols.jsonl"] if is_kinematics_source else ["coverage/kinematics-source-closure.report.json", "coverage/config-resources.jsonl"] if is_kinematics_config else ["wasm-core/include/sim_kinematics.h", "wasm-core/CMakeLists.txt"],
|
||||
"outputs": ["coverage/kinematics-home-limit.report.json"] if is_home_limit_implementation else ["coverage/kinematics-feedback.report.json"] if is_feedback_implementation else ["coverage/kinematics-motion.report.json"] if is_motion_implementation else ["coverage/kinematics-registry.report.json"] if is_registry_implementation else ["coverage/userkins-kinematics.report.json"] if (is_userkins_source or is_userkins_config or is_userkins_implementation) else ["coverage/switchkins-kinematics.report.json"] if (is_switchkins_source or is_switchkins_config) else ["coverage/corexy-hal.report.json"] if is_corexy_hal else ["coverage/tripod-kinematics.report.json"] if (is_tripod_source or is_tripod_config) else ["coverage/genhex-kinematics.report.json"] if (is_genhex_source or is_genhex_config or is_genhex_hal) else ["coverage/pentapod-kinematics.report.json"] if (is_penta_source or is_penta_config or is_penta_hal) else ["coverage/kinematics-abi-contract.json", "coverage/kinematics-mapping-contract.json", "coverage/kinematics-numeric-policy.json"] if is_penta_implementation else ["coverage/delta-kinematics.report.json"] if (is_delta_source or is_delta_config or is_delta_implementation) else ["coverage/kinematics-source-closure.report.json", "coverage/trivkins-integration.report.json", "coverage/coupled-kinematics.report.json", "coverage/fiveaxis-kinematics.report.json", "coverage/trt-kinematics.report.json", "coverage/robot-kinematics.report.json", "coverage/genser-kinematics.report.json"] if is_kinematics_source else ["coverage/kinematics-config-graph.report.json", "coverage/kinematics-oracle-samples.jsonl", "coverage/kinematics-oracle.report.json", "coverage/kinematics-golden-schema.json", "coverage/coupled-kinematics.report.json", "coverage/fiveaxis-kinematics.report.json", "coverage/trt-kinematics.report.json", "coverage/robot-kinematics.report.json", "coverage/genser-kinematics.report.json"] if is_kinematics_config else ["coverage/kinematics-abi-contract.json", "coverage/kinematics-mapping-contract.json", "coverage/kinematics-numeric-policy.json", "coverage/coupled-kinematics.report.json", "coverage/fiveaxis-kinematics.report.json", "coverage/trt-kinematics.report.json", "coverage/robot-kinematics.report.json", "coverage/genser-kinematics.report.json"],
|
||||
"errors": ["blocked:linuxcnc/unit_tests/kinematics missing from locked source"],
|
||||
"test_id": "audit:K2-HAL-01" if is_corexy_hal else "audit:K4-TRIPOD-01" if (is_tripod_source or is_tripod_config) else "audit:K4-HEX-01" if (is_genhex_source or is_genhex_config or is_genhex_hal or is_genhex_implementation) else "audit:K4-DELTA-01" if (is_delta_source or is_delta_config or is_delta_implementation) else "audit:K0-INV-01" if is_kinematics_source else "audit:K0-INV-02" if is_kinematics_config else "audit:K1-CONTRACT-01",
|
||||
"oracle": {"status": "blocked", "report": "coverage/corexy-hal.report.json" if is_corexy_hal else "coverage/tripod-kinematics.report.json" if (is_tripod_source or is_tripod_config) else "coverage/genhex-kinematics.report.json" if (is_genhex_source or is_genhex_config or is_genhex_hal or is_genhex_implementation) else "coverage/delta-kinematics.report.json" if (is_delta_source or is_delta_config or is_delta_implementation) else "coverage/kinematics-source-closure.report.json"},
|
||||
"test_id": "audit:K6-HOME-01" if is_home_limit_implementation else "audit:K6-MOTION-02" if is_feedback_implementation else "audit:K6-MOTION-01" if is_motion_implementation else "audit:K5-CFG-01" if is_registry_implementation else "audit:K5-USER-01" if (is_userkins_source or is_userkins_config or is_userkins_implementation) else "audit:K5-SWITCH-01" if (is_switchkins_source or is_switchkins_config) else "audit:K2-HAL-01" if is_corexy_hal else "audit:K4-TRIPOD-01" if (is_tripod_source or is_tripod_config) else "audit:K4-HEX-01" if (is_genhex_source or is_genhex_config or is_genhex_hal) else "audit:K4-PENTA-01" if (is_penta_source or is_penta_config or is_penta_hal or is_penta_implementation) else "audit:K4-DELTA-01" if (is_delta_source or is_delta_config or is_delta_implementation) else "audit:K0-INV-01" if is_kinematics_source else "audit:K0-INV-02" if is_kinematics_config else "audit:K1-CONTRACT-01",
|
||||
"oracle": {"status": "blocked", "report": "coverage/kinematics-home-limit.report.json" if is_home_limit_implementation else "coverage/kinematics-feedback.report.json" if is_feedback_implementation else "coverage/kinematics-motion.report.json" if is_motion_implementation else "coverage/kinematics-registry.report.json" if is_registry_implementation else "coverage/userkins-kinematics.report.json" if (is_userkins_source or is_userkins_config or is_userkins_implementation) else "coverage/switchkins-kinematics.report.json" if (is_switchkins_source or is_switchkins_config) else "coverage/corexy-hal.report.json" if is_corexy_hal else "coverage/tripod-kinematics.report.json" if (is_tripod_source or is_tripod_config) else "coverage/genhex-kinematics.report.json" if (is_genhex_source or is_genhex_config or is_genhex_hal) else "coverage/pentapod-kinematics.report.json" if (is_penta_source or is_penta_config or is_penta_hal or is_penta_implementation) else "coverage/delta-kinematics.report.json" if (is_delta_source or is_delta_config or is_delta_implementation) else "coverage/kinematics-source-closure.report.json"},
|
||||
"mapping_status": "audited_blocked" if not is_corexy_hal else "verified_partial",
|
||||
})
|
||||
if is_sim_configuration:
|
||||
# K8 covers every simulation resource, including configs that do not
|
||||
# declare KINEMATICS. Keep the family-specific owner above, but link
|
||||
# the shared preflight and its explicit blocked oracle as a second
|
||||
# evidence edge.
|
||||
entry["dependencies"] = list(dict.fromkeys([
|
||||
*[str(value) for value in entry.get("dependencies", [])],
|
||||
"coverage/configuration-regression.report.json",
|
||||
"coverage/config-resources.jsonl",
|
||||
"coverage/kinematics-config-graph.report.json",
|
||||
]))
|
||||
entry["outputs"] = list(dict.fromkeys([
|
||||
*[str(value) for value in entry.get("outputs", [])],
|
||||
"coverage/configuration-regression.report.json",
|
||||
]))
|
||||
entry["coverage_dimensions"] = list(dict.fromkeys([
|
||||
*[str(value) for value in entry.get("coverage_dimensions", [])],
|
||||
"configuration_startup_preflight",
|
||||
"configuration_resource_graph",
|
||||
"configuration_kinematics_entry",
|
||||
]))
|
||||
entry["configuration_regression"] = {
|
||||
"feature_id": "feature:configuration-k8-regression",
|
||||
"test_id": "audit:K8-REG-01",
|
||||
"report": "coverage/configuration-regression.report.json",
|
||||
"oracle": "blocked:official LinuxCNC kinematics/recovery oracle is absent from locked source",
|
||||
}
|
||||
if not (is_kinematics_source or is_kinematics_config or is_kinematics_abi or is_corexy_hal or is_switchkins_source or is_switchkins_config or is_userkins_source or is_userkins_config or is_userkins_implementation or is_motion_implementation or is_feedback_implementation or is_home_limit_implementation):
|
||||
entry.update({
|
||||
"domain": "configuration",
|
||||
"feature_id": "feature:configuration-k8-regression",
|
||||
"errors": ["blocked:configuration preflight retains missing resources and official oracle gap"],
|
||||
"test_id": "audit:K8-REG-01",
|
||||
"oracle": {"status": "blocked", "report": "coverage/configuration-regression.report.json"},
|
||||
"mapping_status": "verified_partial",
|
||||
})
|
||||
return entry
|
||||
|
||||
|
||||
|
||||
110
scripts/generate_switchkins_report.py
Normal file
110
scripts/generate_switchkins_report.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate deterministic native/WASM evidence for switchkins integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
INPUTS = {
|
||||
"switch_source": "linuxcnc/src/emc/kinematics/switchkins.c",
|
||||
"switch_header": "linuxcnc/src/emc/kinematics/switchkins.h",
|
||||
"module_source": "linuxcnc/src/emc/kinematics/xyzac-trt-kins.c",
|
||||
"userk_source": "linuxcnc/src/emc/kinematics/userkfuncs.c",
|
||||
"config": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"postgui_hal": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
"implementation": "wasm-core/src/sim_switchkins.c",
|
||||
"kinematics_header": "wasm-core/include/sim_kinematics.h",
|
||||
"kinematics_implementation": "wasm-core/src/sim_kinematics.c",
|
||||
"test": "wasm-core/tests/sim_switchkins_test.c",
|
||||
}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def run_json(command: list[str], root: Path, schema: str) -> dict[str, object]:
|
||||
result = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"{command[0]} exited {result.returncode}: {result.stderr.strip()}")
|
||||
for line in result.stdout.splitlines():
|
||||
if line.lstrip().startswith("{"):
|
||||
item = json.loads(line)
|
||||
if item.get("schema") == schema:
|
||||
return item
|
||||
raise RuntimeError(f"runner did not emit {schema}")
|
||||
|
||||
|
||||
def parse_config(path: Path) -> dict[str, object]:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
match = re.search(r"^KINEMATICS\s*=\s*([^\s]+)(?:\s+sparm=([^\s]+))?", text, re.MULTILINE)
|
||||
joints = re.search(r"^\s*JOINTS\s*=\s*(\d+)", text, re.MULTILINE)
|
||||
coords = re.search(r"^\s*COORDINATES\s*=\s*([^\s]+)", text, re.MULTILINE)
|
||||
return {
|
||||
"path": INPUTS["config"],
|
||||
"kinematics": match.group(1) if match else None,
|
||||
"sparm": match.group(2) if match and match.group(2) else None,
|
||||
"joints": int(joints.group(1)) if joints else None,
|
||||
"coordinates": coords.group(1) if coords else None,
|
||||
"identityfirst": bool(match and match.group(2) and "identityfirst" in match.group(2)),
|
||||
"switch_pin": "motion.switchkins-type" in text,
|
||||
"modes": {"0": "identity", "1": "xyzac-trt", "2": "userk"},
|
||||
}
|
||||
|
||||
|
||||
def generate(root: Path, output: Path) -> dict[str, object]:
|
||||
root = root.resolve()
|
||||
native = run_json([str(root / "build-native/sim_switchkins_test")], root, "switchkins-samples.v1")
|
||||
wasm = run_json(["node", str(root / "build-wasm/sim_switchkins_wasm_test.js")], root, "switchkins-samples.v1")
|
||||
config = parse_config(root / INPUTS["config"])
|
||||
report = {
|
||||
"schema": "switchkins-kinematics-report.v1",
|
||||
"status": "partial",
|
||||
"implementation_status": "verified",
|
||||
"source_records": {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()},
|
||||
"configuration_candidates": [
|
||||
{**config, "id": "shipped-xyzac-trt-identityfirst", "kind": "real", "provenance": "shipped LinuxCNC simulation"},
|
||||
{"id": "mode-transition-pose-continuity", "kind": "derived", "path": INPUTS["test"], "modes": ["identity", "xyzac-trt"], "purpose": "old-mode forward pose to new-mode inverse remap"},
|
||||
{"id": "userk-slot-missing-module", "kind": "derived", "path": INPUTS["test"], "modes": ["userk"], "purpose": "mode 2 remains fail-closed until static userk is registered"},
|
||||
],
|
||||
"mode_registry": {"slots": [0, 1, 2], "names": {"0": "identity", "1": "xyzac-trt", "2": "userk"}, "startup_mode": 0, "identityfirst": config["identityfirst"]},
|
||||
"state_and_errors": {
|
||||
"pose_continuity": {"identity_to_trt": True, "trt_to_identity": True},
|
||||
"state_hash_stable": native["returned_hash"] == native["returned_repeat_hash"] and native["returned_hash"] != 0,
|
||||
"invalid_mode_error": -707,
|
||||
"missing_module_error": -708,
|
||||
"switch_interrupted_error": -709,
|
||||
"abort_restores_active_mode": native["active_mode"] == 0,
|
||||
},
|
||||
"samples": {
|
||||
"native": native,
|
||||
"wasm": wasm,
|
||||
"byte_identical_json": json.dumps(native, sort_keys=True, separators=(",", ":")) == json.dumps(wasm, sort_keys=True, separators=(",", ":")),
|
||||
"state_hash_identical": [native[key] for key in ("initial_hash", "switched_hash", "returned_hash", "returned_repeat_hash")] == [wasm[key] for key in ("initial_hash", "switched_hash", "returned_hash", "returned_repeat_hash")],
|
||||
},
|
||||
"native_wasm": {"status": "passed", "native_command": "build-native/sim_switchkins_test", "wasm_command": "node build-wasm/sim_switchkins_wasm_test.js"},
|
||||
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source", "blocked_samples": ["type 0 identity", "type 1 xyzac-trt", "type 2 userk", "invalid mode", "missing module", "interrupted switch", "abort recovery"]},
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(json.dumps(report, sort_keys=True, separators=(",", ":")).encode()).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/switchkins-kinematics.report.json"))
|
||||
args = parser.parse_args()
|
||||
report = generate(args.repo_root, args.repo_root / args.output)
|
||||
print(f"switchkins={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
99
scripts/generate_userkins_report.py
Normal file
99
scripts/generate_userkins_report.py
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate deterministic evidence for statically linked userk and ugenser."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
INPUTS = {
|
||||
"userk_source": "linuxcnc/src/emc/kinematics/userkfuncs.c",
|
||||
"ugenser_source": "linuxcnc/src/emc/kinematics/ugenserkins.c",
|
||||
"genser_source": "linuxcnc/src/emc/kinematics/genserkins.c",
|
||||
"genser_functions": "linuxcnc/src/emc/kinematics/genserfuncs.c",
|
||||
"userk_config": "linuxcnc/configs/sim/axis/vismach/scara/scara.ini",
|
||||
"ugenser_config": "linuxcnc/configs/sim/axis/vismach/puma/puma560.ini",
|
||||
"ugenser_dh": "linuxcnc/configs/sim/axis/vismach/puma/puma560_dh.hal",
|
||||
"implementation": "wasm-core/src/sim_userkins.c",
|
||||
"header": "wasm-core/include/sim_kinematics.h",
|
||||
"test": "wasm-core/tests/sim_userkins_test.c",
|
||||
}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def run_json(command: list[str], root: Path) -> dict[str, object]:
|
||||
result = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"{command[0]} exited {result.returncode}: {result.stderr.strip()}")
|
||||
for line in result.stdout.splitlines():
|
||||
if line.lstrip().startswith("{"):
|
||||
item = json.loads(line)
|
||||
if item.get("schema") == "userkins-samples.v1":
|
||||
return item
|
||||
raise RuntimeError("runner did not emit userkins-samples.v1")
|
||||
|
||||
|
||||
def generate(root: Path, output: Path) -> dict[str, object]:
|
||||
root = root.resolve()
|
||||
native = run_json([str(root / "build-native/sim_userkins_test")], root)
|
||||
wasm = run_json(["node", str(root / "build-wasm/sim_userkins_wasm_test.js")], root)
|
||||
userk_config = (root / INPUTS["userk_config"]).read_text(encoding="utf-8")
|
||||
ugenser_config = (root / INPUTS["ugenser_config"]).read_text(encoding="utf-8")
|
||||
ugenser_source = (root / INPUTS["ugenser_source"]).read_text(encoding="utf-8")
|
||||
report = {
|
||||
"schema": "userkins-kinematics-report.v1",
|
||||
"status": "partial",
|
||||
"implementation_status": "verified",
|
||||
"source_records": {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()},
|
||||
"configuration_candidates": [
|
||||
{"id": "shipped-scara-userk-slot", "kind": "real", "path": INPUTS["userk_config"], "scan": "kinstype==2 (userk" in userk_config, "implementation": "identity template", "coordinates": "XYZCAB"},
|
||||
{"id": "shipped-puma560-genser", "kind": "real", "path": INPUTS["ugenser_config"], "scan": "KINEMATICS= genserkins" in ugenser_config, "implementation": "ugenser static adapter over genser", "coordinates": "XYZABC"},
|
||||
{"id": "static-descriptor-regression", "kind": "derived", "path": INPUTS["test"], "implementations": ["userk.identity", "ugenser.static"], "purpose": "ABI/capability checks, FK/IK calls, switch slot binding, and native/WASM comparison"},
|
||||
],
|
||||
"static_contract": {
|
||||
"abi_version": native["abi_version"],
|
||||
"capabilities": {"forward": 1, "inverse": 2, "static": 4, "sample_mask": native["capabilities"]},
|
||||
"implementations": {"1": "identity", "2": "genser"},
|
||||
"switch_binding": {"slot": 2, "active_after_select": native["active_mode"]},
|
||||
"dynamic_host_loading": "forbidden; paths, slash-delimited names, and host shared libraries return SIM_KIN_ERR_DYNAMIC_MODULE",
|
||||
},
|
||||
"ugenser_scope": {
|
||||
"legacy_cli_status": "excluded",
|
||||
"source_warning_present": "Declining usage" in ugenser_source and "functioning in its current state is doubtful" in ugenser_source,
|
||||
"adaptation": "reuse the verified statically linked genser FK/IK implementation; do not port gettimeofday, stdin, or HAL process lifecycle",
|
||||
},
|
||||
"error_boundaries": {"abi_version": -710, "capability": -711, "dynamic_module": -712, "missing_or_uninitialized_module": -708},
|
||||
"samples": {
|
||||
"native": native,
|
||||
"wasm": wasm,
|
||||
"byte_identical_json": json.dumps(native, sort_keys=True, separators=(",", ":")) == json.dumps(wasm, sort_keys=True, separators=(",", ":")),
|
||||
"state_hash_identical": [native["identity"]["state_hash"], native["ugenser"]["state_hash"]] == [wasm["identity"]["state_hash"], wasm["ugenser"]["state_hash"]],
|
||||
"state_hash_stable": native["identity"]["state_hash"] == native["identity"]["repeat_hash"] and native["ugenser"]["state_hash"] == native["ugenser"]["repeat_hash"],
|
||||
},
|
||||
"native_wasm": {"status": "passed", "native_command": "build-native/sim_userkins_test", "wasm_command": "node build-wasm/sim_userkins_wasm_test.js"},
|
||||
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; the deprecated ugenser CLI is not an approved replacement", "blocked_samples": ["userk identity template", "ugenser static adapter", "ABI mismatch", "capability mismatch", "dynamic module rejection", "uninitialized module"]},
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(json.dumps(report, sort_keys=True, separators=(",", ":")).encode()).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/userkins-kinematics.report.json"))
|
||||
args = parser.parse_args()
|
||||
report = generate(args.repo_root, args.repo_root / args.output)
|
||||
print(f"userkins={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
52
scripts/test_configuration_regression.py
Normal file
52
scripts/test_configuration_regression.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from generate_configuration_regression import generate
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPORT = ROOT / "coverage/configuration-regression.report.json"
|
||||
|
||||
|
||||
class ConfigurationRegressionTests(unittest.TestCase):
|
||||
def test_report_contract(self):
|
||||
report = json.loads(REPORT.read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["schema"], "configuration-regression-report.v1")
|
||||
self.assertEqual(report["implementation_status"], "verified")
|
||||
self.assertEqual(report["counts"]["registered_configuration_entries"], 264)
|
||||
self.assertEqual(len(report["configuration_entries"]), 264)
|
||||
self.assertEqual(report["closure"]["graph_orphan_entry_ids"], [])
|
||||
self.assertEqual(report["closure"]["unowned_template_ids"], [])
|
||||
self.assertEqual(report["closure"]["resource_orphan_entrypoints"], [])
|
||||
self.assertEqual(report["runtime"]["native_wasm"]["status"], "passed")
|
||||
self.assertEqual(report["runtime"]["worker"]["status"], "passed")
|
||||
self.assertEqual(report["recovery_summary"]["endpoints_equal"], True)
|
||||
self.assertEqual(report["oracle"]["status"], "blocked")
|
||||
self.assertTrue(report["oracle"]["derived_configuration_results_are_not_official"])
|
||||
|
||||
def test_all_entry_ids_are_unique_and_repeat_stable(self):
|
||||
report = json.loads(REPORT.read_text(encoding="utf-8"))
|
||||
ids = [row["config_id"] for row in report["configuration_entries"]]
|
||||
self.assertEqual(len(ids), len(set(ids)))
|
||||
for group in (report["runtime"]["native"], report["runtime"]["wasm"]):
|
||||
self.assertEqual(group["status"], "passed")
|
||||
self.assertTrue(all(row["repeat_stable"] for row in group["records"].values()))
|
||||
self.assertTrue(all(row["repeat_stable"] for row in report["runtime"]["worker"]["records"].values()))
|
||||
|
||||
def test_generation_is_deterministic(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
first_path = Path(directory) / "first.json"
|
||||
second_path = Path(directory) / "second.json"
|
||||
first = generate(ROOT, first_path)
|
||||
second = generate(ROOT, second_path)
|
||||
self.assertEqual(first, second)
|
||||
self.assertEqual(first_path.read_bytes(), second_path.read_bytes())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -34,6 +34,10 @@ class HalRuntimeTests(unittest.TestCase):
|
||||
self.assertEqual(report["schema"], "corexy-hal-report.v1")
|
||||
self.assertTrue(report["native_wasm"]["byte_identical_json"])
|
||||
|
||||
def test_io_pin_capacity_is_declared(self):
|
||||
report = json.loads((ROOT / "coverage/hal-runtime.report.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["pin_capacity"], 256)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
45
scripts/test_io_report.py
Normal file
45
scripts/test_io_report.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import generate_io_report
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class IoReportTests(unittest.TestCase):
|
||||
def test_contract(self):
|
||||
report = json.loads((ROOT / "coverage/io-runtime.report.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["schema"], "io-runtime-report.v1")
|
||||
self.assertEqual(report["implementation_status"], "verified")
|
||||
self.assertEqual(report["boundaries"]["invalid_pin_type"], -304)
|
||||
self.assertEqual(report["boundaries"]["spindle_limit"], -906)
|
||||
self.assertEqual(report["boundaries"]["tool_change_while_spinning"], -910)
|
||||
self.assertEqual(report["boundaries"]["probe_trigger"], 0)
|
||||
self.assertEqual(report["boundaries"]["probe_state"], 2)
|
||||
self.assertEqual(report["boundaries"]["estop_command"], -904)
|
||||
self.assertEqual(report["boundaries"]["estop_recovery_before_clear"], -904)
|
||||
self.assertTrue(report["boundaries"]["state_hash_stable"])
|
||||
self.assertTrue(report["samples"]["byte_identical_json"])
|
||||
self.assertTrue(report["samples"]["state_hash_identical"])
|
||||
self.assertEqual(report["oracle"]["status"], "blocked")
|
||||
|
||||
def test_deterministic(self):
|
||||
first, second = ROOT / "coverage/.io-one.json", ROOT / "coverage/.io-two.json"
|
||||
try:
|
||||
self.assertEqual(generate_io_report.generate(ROOT, first), generate_io_report.generate(ROOT, second))
|
||||
finally:
|
||||
first.unlink(missing_ok=True)
|
||||
second.unlink(missing_ok=True)
|
||||
|
||||
def test_wasm_exports_io_api(self):
|
||||
module = ROOT / "build-wasm/linuxcnc-sim-core.js"
|
||||
if not module.is_file():
|
||||
self.skipTest("WASM build has not been generated")
|
||||
text = module.read_text(encoding="utf-8")
|
||||
for symbol in ("sim_io_init", "sim_io_spindle_command", "sim_io_tool_change", "sim_io_probe_update", "sim_io_recover"):
|
||||
self.assertIn(symbol, text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
34
scripts/test_kinematics_feedback_report.py
Normal file
34
scripts/test_kinematics_feedback_report.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import generate_kinematics_feedback_report
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class KinematicsFeedbackReportTests(unittest.TestCase):
|
||||
def test_contract(self):
|
||||
report = json.loads((ROOT / "coverage/kinematics-feedback.report.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["schema"], "kinematics-feedback-report.v1")
|
||||
self.assertEqual(report["implementation_status"], "verified")
|
||||
self.assertEqual(report["boundaries"]["stale_read"], -824)
|
||||
self.assertEqual(report["boundaries"]["nonfinite"], -822)
|
||||
self.assertEqual(report["boundaries"]["forward_failure"], -823)
|
||||
self.assertEqual(report["boundaries"]["failed_feedback_valid"], 0)
|
||||
self.assertTrue(report["boundaries"]["state_hash_stable"])
|
||||
self.assertTrue(report["samples"]["byte_identical_json"])
|
||||
self.assertTrue(report["samples"]["state_hash_identical"])
|
||||
self.assertEqual(report["oracle"]["status"], "blocked")
|
||||
|
||||
def test_deterministic(self):
|
||||
first, second = ROOT / "coverage/.feedback-one.json", ROOT / "coverage/.feedback-two.json"
|
||||
try:
|
||||
self.assertEqual(generate_kinematics_feedback_report.generate(ROOT, first), generate_kinematics_feedback_report.generate(ROOT, second))
|
||||
finally:
|
||||
first.unlink(missing_ok=True)
|
||||
second.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
34
scripts/test_kinematics_home_limit_report.py
Normal file
34
scripts/test_kinematics_home_limit_report.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import generate_kinematics_home_limit_report
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class KinematicsHomeLimitReportTests(unittest.TestCase):
|
||||
def test_contract(self):
|
||||
report = json.loads((ROOT / "coverage/kinematics-home-limit.report.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["schema"], "kinematics-home-limit-report.v1")
|
||||
self.assertEqual(report["implementation_status"], "verified")
|
||||
self.assertEqual(report["boundaries"]["unhomed"], -842)
|
||||
self.assertEqual(report["boundaries"]["sequence"], -846)
|
||||
self.assertEqual(report["boundaries"]["limit"], -843)
|
||||
self.assertTrue(report["boundaries"]["retains_valid_pose"])
|
||||
self.assertTrue(report["boundaries"]["state_hash_stable"])
|
||||
self.assertTrue(report["samples"]["byte_identical_json"])
|
||||
self.assertTrue(report["samples"]["state_hash_identical"])
|
||||
self.assertEqual(report["oracle"]["status"], "blocked")
|
||||
|
||||
def test_deterministic(self):
|
||||
first, second = ROOT / "coverage/.home-one.json", ROOT / "coverage/.home-two.json"
|
||||
try:
|
||||
self.assertEqual(generate_kinematics_home_limit_report.generate(ROOT, first), generate_kinematics_home_limit_report.generate(ROOT, second))
|
||||
finally:
|
||||
first.unlink(missing_ok=True)
|
||||
second.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
35
scripts/test_kinematics_motion_report.py
Normal file
35
scripts/test_kinematics_motion_report.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import generate_kinematics_motion_report
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class KinematicsMotionReportTests(unittest.TestCase):
|
||||
def test_contract(self):
|
||||
report = json.loads((ROOT / "coverage/kinematics-motion.report.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["schema"], "kinematics-motion-report.v1")
|
||||
self.assertEqual(report["implementation_status"], "verified")
|
||||
self.assertEqual(report["pipeline"]["joint_queue_capacity"], 64)
|
||||
self.assertEqual(report["boundaries"]["capacity"], -802)
|
||||
self.assertEqual(report["boundaries"]["inverse_failure"], -804)
|
||||
self.assertTrue(report["boundaries"]["capacity_planner_rollback"])
|
||||
self.assertTrue(report["boundaries"]["inverse_planner_rollback"])
|
||||
self.assertTrue(report["boundaries"]["state_hash_stable"])
|
||||
self.assertTrue(report["samples"]["byte_identical_json"])
|
||||
self.assertTrue(report["samples"]["state_hash_identical"])
|
||||
self.assertEqual(report["oracle"]["status"], "blocked")
|
||||
|
||||
def test_deterministic(self):
|
||||
first, second = ROOT / "coverage/.motion-one.json", ROOT / "coverage/.motion-two.json"
|
||||
try:
|
||||
self.assertEqual(generate_kinematics_motion_report.generate(ROOT, first), generate_kinematics_motion_report.generate(ROOT, second))
|
||||
finally:
|
||||
first.unlink(missing_ok=True)
|
||||
second.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
33
scripts/test_kinematics_registry_report.py
Normal file
33
scripts/test_kinematics_registry_report.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import generate_kinematics_registry_report
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class KinematicsRegistryReportTests(unittest.TestCase):
|
||||
def test_contract(self):
|
||||
report = json.loads((ROOT / "coverage/kinematics-registry.report.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["schema"], "kinematics-registry-report.v1")
|
||||
self.assertEqual(report["implementation_status"], "verified")
|
||||
self.assertEqual(report["registry"]["entry_count"], 18)
|
||||
self.assertEqual(report["registry"]["verified_entries"], 18)
|
||||
self.assertEqual(report["registry"]["capability_union"], 127)
|
||||
self.assertEqual(report["registry"]["parameter_union"], 255)
|
||||
self.assertTrue(report["samples"]["byte_identical_json"])
|
||||
self.assertTrue(report["samples"]["state_hash_identical"])
|
||||
self.assertEqual(report["oracle"]["status"], "blocked")
|
||||
|
||||
def test_deterministic(self):
|
||||
first, second = ROOT / "coverage/.registry-one.json", ROOT / "coverage/.registry-two.json"
|
||||
try:
|
||||
self.assertEqual(generate_kinematics_registry_report.generate(ROOT, first), generate_kinematics_registry_report.generate(ROOT, second))
|
||||
finally:
|
||||
first.unlink(missing_ok=True)
|
||||
second.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,6 +12,10 @@ class PentapodKinematicsReportTests(unittest.TestCase):
|
||||
self.assertEqual(report["implementation_status"], "verified")
|
||||
self.assertTrue(report["configuration_candidate"]["scan"])
|
||||
self.assertEqual(report["geometry"]["struts"], 5)
|
||||
self.assertEqual(report["mapping"]["coordinates"], "XYZAB")
|
||||
self.assertEqual(report["workspace_and_boundaries"]["singular_jacobian_error"], -704)
|
||||
self.assertEqual(report["workspace_and_boundaries"]["nonpositive_joint_error"], -702)
|
||||
self.assertEqual(len(report["configuration_candidates"]), 2)
|
||||
self.assertTrue(report["samples"]["byte_identical_json"])
|
||||
self.assertTrue(report["samples"]["state_hash_identical"])
|
||||
self.assertEqual(report["oracle"]["status"], "blocked")
|
||||
|
||||
37
scripts/test_switchkins_report.py
Normal file
37
scripts/test_switchkins_report.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import generate_switchkins_report
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class SwitchkinsReportTests(unittest.TestCase):
|
||||
def test_contract(self):
|
||||
report = json.loads((ROOT / "coverage/switchkins-kinematics.report.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["schema"], "switchkins-kinematics-report.v1")
|
||||
self.assertEqual(report["implementation_status"], "verified")
|
||||
self.assertTrue(report["mode_registry"]["identityfirst"])
|
||||
self.assertEqual(report["mode_registry"]["names"]["2"], "userk")
|
||||
self.assertTrue(report["state_and_errors"]["pose_continuity"]["identity_to_trt"])
|
||||
self.assertTrue(report["state_and_errors"]["state_hash_stable"])
|
||||
self.assertEqual(report["state_and_errors"]["invalid_mode_error"], -707)
|
||||
self.assertEqual(report["state_and_errors"]["missing_module_error"], -708)
|
||||
self.assertEqual(report["state_and_errors"]["switch_interrupted_error"], -709)
|
||||
self.assertTrue(report["samples"]["byte_identical_json"])
|
||||
self.assertTrue(report["samples"]["state_hash_identical"])
|
||||
self.assertEqual(report["oracle"]["status"], "blocked")
|
||||
|
||||
def test_deterministic(self):
|
||||
first, second = ROOT / "coverage/.switch-one.json", ROOT / "coverage/.switch-two.json"
|
||||
try:
|
||||
self.assertEqual(generate_switchkins_report.generate(ROOT, first), generate_switchkins_report.generate(ROOT, second))
|
||||
finally:
|
||||
first.unlink(missing_ok=True)
|
||||
second.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
36
scripts/test_userkins_report.py
Normal file
36
scripts/test_userkins_report.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import generate_userkins_report
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class UserkinsReportTests(unittest.TestCase):
|
||||
def test_contract(self):
|
||||
report = json.loads((ROOT / "coverage/userkins-kinematics.report.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["schema"], "userkins-kinematics-report.v1")
|
||||
self.assertEqual(report["implementation_status"], "verified")
|
||||
self.assertEqual(report["static_contract"]["abi_version"], 1)
|
||||
self.assertEqual(report["static_contract"]["switch_binding"]["active_after_select"], 2)
|
||||
self.assertEqual(report["ugenser_scope"]["legacy_cli_status"], "excluded")
|
||||
self.assertTrue(report["ugenser_scope"]["source_warning_present"])
|
||||
self.assertEqual(report["error_boundaries"]["dynamic_module"], -712)
|
||||
self.assertTrue(report["samples"]["byte_identical_json"])
|
||||
self.assertTrue(report["samples"]["state_hash_identical"])
|
||||
self.assertTrue(report["samples"]["state_hash_stable"])
|
||||
self.assertEqual(report["oracle"]["status"], "blocked")
|
||||
|
||||
def test_deterministic(self):
|
||||
first, second = ROOT / "coverage/.user-one.json", ROOT / "coverage/.user-two.json"
|
||||
try:
|
||||
self.assertEqual(generate_userkins_report.generate(ROOT, first), generate_userkins_report.generate(ROOT, second))
|
||||
finally:
|
||||
first.unlink(missing_ok=True)
|
||||
second.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -154,6 +154,56 @@ def validate(repo_root: Path, lock_path: Path, source_path: Path, manifest_path:
|
||||
errors.append("workspace recovery report is missing")
|
||||
elif recovery_report_hash != sha256(recovery_report_path):
|
||||
errors.append("workspace recovery report hash does not match context lock")
|
||||
if recovery_report_path.is_file():
|
||||
try:
|
||||
recovery_report = json.loads(recovery_report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
errors.append("workspace recovery report is not valid JSON")
|
||||
else:
|
||||
if recovery_report.get("status") != "passed":
|
||||
errors.append("workspace recovery report is not passed")
|
||||
if recovery_report.get("implementation_status") != "verified":
|
||||
errors.append("workspace recovery implementation is not verified")
|
||||
if recovery_report.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("workspace recovery oracle status must remain explicitly blocked")
|
||||
configuration_regression_path = repo_root / "coverage/configuration-regression.report.json"
|
||||
configuration_regression_hash = lock.get("configuration_regression_report_sha256")
|
||||
if configuration_regression_hash:
|
||||
if not configuration_regression_path.is_file():
|
||||
errors.append("configuration regression report is missing")
|
||||
elif configuration_regression_hash != sha256(configuration_regression_path):
|
||||
errors.append("configuration regression report hash does not match context lock")
|
||||
if configuration_regression_path.is_file():
|
||||
try:
|
||||
configuration_regression = json.loads(configuration_regression_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
errors.append("configuration regression report is not valid JSON")
|
||||
else:
|
||||
if configuration_regression.get("implementation_status") != "verified":
|
||||
errors.append("configuration regression implementation is not verified")
|
||||
if configuration_regression.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("configuration regression oracle status must remain explicitly blocked")
|
||||
closure = configuration_regression.get("closure", {})
|
||||
if closure.get("graph_orphan_entry_ids") != []:
|
||||
errors.append("configuration regression graph contains orphan entries")
|
||||
if closure.get("unowned_template_ids") != []:
|
||||
errors.append("configuration regression graph contains unowned templates")
|
||||
if closure.get("resource_orphan_entrypoints") != []:
|
||||
errors.append("configuration regression resource graph contains orphan entrypoints")
|
||||
if configuration_regression.get("runtime", {}).get("native_wasm", {}).get("status") != "passed":
|
||||
errors.append("configuration regression native/WASM summary is not passed")
|
||||
if configuration_regression.get("runtime", {}).get("worker", {}).get("status") != "passed":
|
||||
errors.append("configuration regression Worker summary is not passed")
|
||||
if isinstance(inventory_count, dict):
|
||||
report_counts = configuration_regression.get("counts", {})
|
||||
for inventory_key, report_key in (
|
||||
("configuration_regression_entries", "registered_configuration_entries"),
|
||||
("configuration_regression_entrypoints", "config_entrypoints"),
|
||||
("configuration_regression_resources", "resource_rows"),
|
||||
):
|
||||
value = report_counts.get(report_key)
|
||||
if value is not None and inventory_count.get(inventory_key) != value:
|
||||
errors.append(f"{inventory_key} count does not match configuration regression report")
|
||||
runtime_report_path = repo_root / "coverage/runtime-mapping.report.json"
|
||||
runtime_report_hash = lock.get("runtime_mapping_report_sha256")
|
||||
if runtime_report_hash:
|
||||
@@ -203,6 +253,13 @@ def validate(repo_root: Path, lock_path: Path, source_path: Path, manifest_path:
|
||||
errors.append("motion runtime report is missing")
|
||||
elif motion_report_hash != sha256(motion_report_path):
|
||||
errors.append("motion runtime report hash does not match context lock")
|
||||
io_report_path = repo_root / "coverage/io-runtime.report.json"
|
||||
io_report_hash = lock.get("io_runtime_report_sha256")
|
||||
if io_report_hash:
|
||||
if not io_report_path.is_file():
|
||||
errors.append("io runtime report is missing")
|
||||
elif io_report_hash != sha256(io_report_path):
|
||||
errors.append("io runtime report hash does not match context lock")
|
||||
kinematics_runtime_report_path = repo_root / "coverage/kinematics-runtime.report.json"
|
||||
kinematics_runtime_report_hash = lock.get("kinematics_runtime_report_sha256")
|
||||
if kinematics_runtime_report_hash:
|
||||
@@ -484,6 +541,125 @@ def validate(repo_root: Path, lock_path: Path, source_path: Path, manifest_path:
|
||||
errors.append("genhex kinematics implementation is not verified")
|
||||
if genhex_report.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("genhex kinematics oracle status must remain explicitly blocked")
|
||||
pentapod_report_path = repo_root / "coverage/pentapod-kinematics.report.json"
|
||||
pentapod_report_hash = lock.get("pentapod_kinematics_report_sha256")
|
||||
if pentapod_report_hash:
|
||||
if not pentapod_report_path.is_file():
|
||||
errors.append("pentapod kinematics report is missing")
|
||||
elif pentapod_report_hash != sha256(pentapod_report_path):
|
||||
errors.append("pentapod kinematics report hash does not match context lock")
|
||||
if pentapod_report_path.is_file():
|
||||
try:
|
||||
pentapod_report = json.loads(pentapod_report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
errors.append("pentapod kinematics report is not valid JSON")
|
||||
else:
|
||||
if pentapod_report.get("implementation_status") != "verified":
|
||||
errors.append("pentapod kinematics implementation is not verified")
|
||||
if pentapod_report.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("pentapod kinematics oracle status must remain explicitly blocked")
|
||||
switchkins_report_path = repo_root / "coverage/switchkins-kinematics.report.json"
|
||||
switchkins_report_hash = lock.get("switchkins_kinematics_report_sha256")
|
||||
if switchkins_report_hash:
|
||||
if not switchkins_report_path.is_file():
|
||||
errors.append("switchkins kinematics report is missing")
|
||||
elif switchkins_report_hash != sha256(switchkins_report_path):
|
||||
errors.append("switchkins kinematics report hash does not match context lock")
|
||||
if switchkins_report_path.is_file():
|
||||
try:
|
||||
switchkins_report = json.loads(switchkins_report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
errors.append("switchkins kinematics report is not valid JSON")
|
||||
else:
|
||||
if switchkins_report.get("implementation_status") != "verified":
|
||||
errors.append("switchkins kinematics implementation is not verified")
|
||||
if switchkins_report.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("switchkins kinematics oracle status must remain explicitly blocked")
|
||||
userkins_report_path = repo_root / "coverage/userkins-kinematics.report.json"
|
||||
userkins_report_hash = lock.get("userkins_kinematics_report_sha256")
|
||||
if userkins_report_hash:
|
||||
if not userkins_report_path.is_file():
|
||||
errors.append("userkins kinematics report is missing")
|
||||
elif userkins_report_hash != sha256(userkins_report_path):
|
||||
errors.append("userkins kinematics report hash does not match context lock")
|
||||
if userkins_report_path.is_file():
|
||||
try:
|
||||
userkins_report = json.loads(userkins_report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
errors.append("userkins kinematics report is not valid JSON")
|
||||
else:
|
||||
if userkins_report.get("implementation_status") != "verified":
|
||||
errors.append("userkins kinematics implementation is not verified")
|
||||
if userkins_report.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("userkins kinematics oracle status must remain explicitly blocked")
|
||||
registry_report_path = repo_root / "coverage/kinematics-registry.report.json"
|
||||
registry_report_hash = lock.get("kinematics_registry_report_sha256")
|
||||
if registry_report_hash:
|
||||
if not registry_report_path.is_file():
|
||||
errors.append("kinematics registry report is missing")
|
||||
elif registry_report_hash != sha256(registry_report_path):
|
||||
errors.append("kinematics registry report hash does not match context lock")
|
||||
if registry_report_path.is_file():
|
||||
try:
|
||||
registry_report = json.loads(registry_report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
errors.append("kinematics registry report is not valid JSON")
|
||||
else:
|
||||
if registry_report.get("implementation_status") != "verified":
|
||||
errors.append("kinematics registry implementation is not verified")
|
||||
if registry_report.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("kinematics registry oracle status must remain explicitly blocked")
|
||||
motion_report_path = repo_root / "coverage/kinematics-motion.report.json"
|
||||
motion_report_hash = lock.get("kinematics_motion_report_sha256")
|
||||
if motion_report_hash:
|
||||
if not motion_report_path.is_file():
|
||||
errors.append("kinematics motion report is missing")
|
||||
elif motion_report_hash != sha256(motion_report_path):
|
||||
errors.append("kinematics motion report hash does not match context lock")
|
||||
if motion_report_path.is_file():
|
||||
try:
|
||||
motion_report = json.loads(motion_report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
errors.append("kinematics motion report is not valid JSON")
|
||||
else:
|
||||
if motion_report.get("implementation_status") != "verified":
|
||||
errors.append("kinematics motion implementation is not verified")
|
||||
if motion_report.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("kinematics motion oracle status must remain explicitly blocked")
|
||||
feedback_report_path = repo_root / "coverage/kinematics-feedback.report.json"
|
||||
feedback_report_hash = lock.get("kinematics_feedback_report_sha256")
|
||||
if feedback_report_hash:
|
||||
if not feedback_report_path.is_file():
|
||||
errors.append("kinematics feedback report is missing")
|
||||
elif feedback_report_hash != sha256(feedback_report_path):
|
||||
errors.append("kinematics feedback report hash does not match context lock")
|
||||
if feedback_report_path.is_file():
|
||||
try:
|
||||
feedback_report = json.loads(feedback_report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
errors.append("kinematics feedback report is not valid JSON")
|
||||
else:
|
||||
if feedback_report.get("implementation_status") != "verified":
|
||||
errors.append("kinematics feedback implementation is not verified")
|
||||
if feedback_report.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("kinematics feedback oracle status must remain explicitly blocked")
|
||||
home_limit_report_path = repo_root / "coverage/kinematics-home-limit.report.json"
|
||||
home_limit_report_hash = lock.get("kinematics_home_limit_report_sha256")
|
||||
if home_limit_report_hash:
|
||||
if not home_limit_report_path.is_file():
|
||||
errors.append("kinematics home limit report is missing")
|
||||
elif home_limit_report_hash != sha256(home_limit_report_path):
|
||||
errors.append("kinematics home limit report hash does not match context lock")
|
||||
if home_limit_report_path.is_file():
|
||||
try:
|
||||
home_limit_report = json.loads(home_limit_report_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
errors.append("kinematics home limit report is not valid JSON")
|
||||
else:
|
||||
if home_limit_report.get("implementation_status") != "verified":
|
||||
errors.append("kinematics home limit implementation is not verified")
|
||||
if home_limit_report.get("oracle", {}).get("status") != "blocked":
|
||||
errors.append("kinematics home limit oracle status must remain explicitly blocked")
|
||||
source_ids = [str(row.get("source_id", "")) for row in sources]
|
||||
if len(source_ids) != len(set(source_ids)):
|
||||
errors.append("source index contains duplicate source_id")
|
||||
|
||||
Reference in New Issue
Block a user