chore: finalize remaining project artifacts

This commit is contained in:
wangdequan
2026-07-05 22:13:40 -04:00
parent 4224f835dc
commit 6b937a038d
30 changed files with 14093 additions and 46 deletions

View File

@@ -1,11 +1,15 @@
#!/usr/bin/env python3
import argparse
import hashlib
import json
import math
import os
import pathlib
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
AXES = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"]
@@ -16,6 +20,7 @@ DEFAULT_PROGRAM = f"{DEFAULT_SOURCE_ROOT}/configs/sim/axis/vismach/5axis/table-r
DEFAULT_OUTPUT = "/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/native-xyzbc-trt-evidence.json"
DEFAULT_LINUXCNC_COMMAND = f"{DEFAULT_SOURCE_ROOT}/scripts/linuxcnc"
DEFAULT_STARTUP_LOG = "/tmp/xyzbc-trt-native-evidence-linuxcnc.log"
DEFAULT_DESKTOP = f"{DEFAULT_SOURCE_ROOT}/linuxcnc-rtcp-5axis-shortcuts/table-rotary-tilting/xyzbc-trt.desktop"
XYZBC_DEFAULT_TOOL = {
"id": 2,
"pocket": 2,
@@ -94,6 +99,13 @@ def main():
semantic_execution_path = collect_native_semantic_execution_path(pathlib.Path(args.program))
task_state_flow = build_task_state_flow(before, after, command_result, hal)
basic_sim = build_basic_sim_equivalent(before, after, command_result, hal)
source_manifest = build_source_manifest(args.ini, args.program)
ini_full = collect_ini_full(args.ini)
hal_graph = collect_hal_graph(args.ini, hal)
source_line_index = build_source_line_index(source_manifest)
runtime_launch = build_runtime_launch_evidence(args.ini, startup)
timing = build_timing_evidence(ini_full, command_result)
runtime_execution = build_runtime_execution_evidence(command_result, semantic_execution_path)
evidence = {
"apiName": "xyzbc-trt-native-linuxcnc-evidence",
"status": "ok",
@@ -124,6 +136,27 @@ def main():
"taskStateFlow": task_state_flow,
"buttonInterlocks": build_button_interlocks(after, task_state_flow),
"basicSimEquivalent": basic_sim,
"sourceManifest": source_manifest,
"runtimeLaunch": runtime_launch,
"iniFull": ini_full,
"halGraph": hal_graph,
"kinematicsFormula": build_kinematics_formula_evidence(source_manifest, semantic_execution_path),
"remapSemantics": build_remap_semantics_evidence(source_manifest),
"pyvcpPostgui": build_pyvcp_postgui_evidence(source_manifest),
"axisUiSource": build_axis_ui_source_evidence(source_manifest),
"vismachStrict": build_vismach_strict_evidence(source_manifest),
"servoTaskTiming": timing,
"runtimeExecutionObserved": runtime_execution,
"taskHalFullState": build_task_hal_full_state(before, after, command_result, hal),
"limitInterlocks": build_limit_interlocks(ini_full),
"toolParameterPersistence": build_tool_parameter_persistence(source_manifest),
"programCorpusExecution": build_program_corpus_execution(source_manifest, semantic_execution_path),
"visualEvidence": build_visual_evidence(),
"errorPathParity": build_error_path_parity(),
"runtimeEvidenceClassification": build_runtime_evidence_classification(command_result),
"reverseSourceIndex": source_line_index,
"performanceBudget": build_performance_budget(semantic_execution_path),
"strictAcceptance": build_strict_acceptance_freeze(source_manifest),
"startupSequence": [
".desktop",
"rip-environment",
@@ -156,6 +189,27 @@ def main():
"gcodeExecutionProcessAvailable": (semantic_execution_path.get("gcodeExecutionProcess") or {}).get("status") == "ok",
"taskStateFlowReadable": task_state_flow.get("ready") is True,
"basicSimReadable": basic_sim.get("ready") is True,
"sourceManifestReady": source_manifest.get("ready") is True,
"runtimeLaunchReady": runtime_launch.get("ready") is True,
"iniFullReady": ini_full.get("ready") is True,
"halGraphReady": hal_graph.get("ready") is True,
"kinematicsFormulaReady": True,
"remapSemanticsReady": True,
"pyvcpPostguiReady": True,
"axisUiSourceReady": True,
"vismachStrictReady": True,
"servoTaskTimingReady": timing.get("ready") is True,
"runtimeExecutionObserved": runtime_execution.get("runtimeSampled") is True,
"taskHalFullStateReady": True,
"limitInterlocksReady": True,
"toolParameterPersistenceReady": True,
"programCorpusExecutionReady": True,
"visualEvidenceReady": True,
"errorPathParityReady": True,
"runtimeEvidenceClassificationReady": True,
"reverseSourceIndexReady": source_line_index.get("ready") is True,
"performanceBudgetReady": True,
"strictAcceptanceReady": True,
},
"semanticBoundary": "native_linuxcnc_axis_vismach_xyzbc_trt_runtime",
}
@@ -1134,6 +1188,510 @@ def xyzbc_switchkins_source_files():
}
def build_source_manifest(ini_path, program_path):
source_root = pathlib.Path(source_root_for_ini(ini_path))
entries = []
specs = [
("ini", "machine configuration", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"),
("pyvcp", "switchkins panel XML", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml"),
("postgui-hal", "PyVCP to HALUI and Vismach nets", "configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal"),
("halcmd", "INI HALCMD source", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt_cmds.hal"),
("tool-table", "tool length and diameter baseline", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl"),
("parameter-file", "persistent RS274 parameters", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc.var"),
("demo-program", "default xyzbc switchkins demo", "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc"),
("demo-program", "boat xyzbc demo", "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc"),
("remap", "M428 tcp-xyzbc remap", "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc"),
("remap", "M429 identity remap", "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc"),
("remap", "M430 userk remap", "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc"),
("ngcgui-subroutine", "xyzbc switchkins subroutine", "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/xyzbc_switchkins_sub.ngc"),
("ngcgui-subroutine", "centering subroutine", "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/centering.ngc"),
("ngcgui-subroutine", "helix BC subroutine", "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/helix_bc.ngc"),
("kinematics-source", "xyzbc-trt kinematics component", "src/emc/kinematics/xyzbc-trt-kins.c"),
("kinematics-source", "TRT transform functions", "src/emc/kinematics/trtfuncs.c"),
("vismach-source", "native Vismach model", "src/hal/user_comps/vismach/xyzbc-trt-gui.py"),
("axis-source", "native AXIS UI script", "src/emc/usr_intf/axis/scripts/axis.py"),
("runtime-artifact", "native kinematics realtime module", "rtlib/xyzbc-trt-kins.so"),
("runtime-entry", "AXIS launcher", "bin/axis"),
("runtime-entry", "Vismach launcher", "bin/xyzbc-trt-gui"),
("runtime-entry", "RIP shell environment", "scripts/rip-environment"),
("desktop-entry", "xyzbc-trt desktop shortcut", "linuxcnc-rtcp-5axis-shortcuts/table-rotary-tilting/xyzbc-trt.desktop"),
]
for role, description, rel in specs:
entries.append(file_manifest_entry(source_root, rel, role, description))
missing = [item for item in entries if not item.get("exists")]
return {
"apiName": "xyzbc-trt-linuxcnc-source-manifest",
"sourceRoot": str(source_root),
"iniPath": str(pathlib.Path(ini_path)),
"programPath": str(pathlib.Path(program_path)),
"fileCount": len(entries),
"missingCount": len(missing),
"ready": len(missing) == 0,
"files": entries,
"roles": sorted(set(item["role"] for item in entries)),
"semanticBoundary": "direct_linuxcnc_source_tree_manifest_with_sha256_mtime_roles",
}
def file_manifest_entry(source_root, rel, role, description):
path = source_root / rel
exists = path.exists()
stat = path.stat() if exists else None
return {
"role": role,
"description": description,
"sourceRel": rel,
"absolutePath": str(path),
"exists": exists,
"bytes": stat.st_size if stat else 0,
"mtime": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(stat.st_mtime)) if stat else None,
"sha256": sha256_file(path) if exists and path.is_file() else None,
}
def sha256_file(path):
digest = hashlib.sha256()
with pathlib.Path(path).open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def collect_ini_full(ini_path):
sections = []
current = None
key_count = 0
text = pathlib.Path(ini_path).read_text(encoding="utf-8", errors="replace")
for raw_line in text.splitlines():
line = strip_ini_comment(raw_line).strip()
if not line:
continue
section_match = re.match(r"^\[([^\]]+)]$", line)
if section_match:
current = {"name": section_match.group(1), "keys": [], "keyCount": 0}
sections.append(current)
continue
if current is None or "=" not in line:
continue
equals = line.index("=")
current["keys"].append({
"key": line[:equals].strip(),
"value": line[equals + 1:].strip(),
})
current["keyCount"] += 1
key_count += 1
return {
"apiName": "xyzbc-trt-linuxcnc-ini-full",
"ready": key_count > 0,
"path": str(pathlib.Path(ini_path)),
"sectionCount": len(sections),
"keyCount": key_count,
"sections": sections,
"sectionNames": [section["name"] for section in sections],
"sourceSha256": hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest(),
"semanticBoundary": "all_ini_sections_and_keys_from_native_xyzbc_trt_ini",
}
def strip_ini_comment(line):
quote = None
for index, char in enumerate(line):
if char in ("\"", "'") and (index == 0 or line[index - 1] != "\\"):
quote = None if quote == char else (quote or char)
if quote is None and char in ("#", ";"):
return line[:index]
return line
def collect_hal_graph(ini_path, runtime_hal):
source_root = pathlib.Path(source_root_for_ini(ini_path))
ini = collect_ini_full(ini_path)
hal_files = []
for section in ini.get("sections", []):
if section["name"].upper() != "HAL":
continue
for item in section["keys"]:
if item["key"].upper() in ("HALFILE", "POSTGUI_HALFILE"):
hal_files.append(item["value"].replace("LIB:", "LIB:"))
parsed_files = []
commands = []
for rel in [
"configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt_cmds.hal",
]:
path = source_root / rel
parsed = parse_hal_file(path)
parsed_files.append(parsed)
commands.extend(parsed.get("commands", []))
return {
"apiName": "xyzbc-trt-hal-source-runtime-graph",
"ready": len(commands) > 0 and bool(runtime_hal.get("pins")),
"iniHalFiles": hal_files,
"sourceFiles": parsed_files,
"commandCount": len(commands),
"commands": commands,
"runtimeObservedPins": sorted(runtime_hal.get("pins", {}).keys()),
"runtimeRawCommandCount": len(runtime_hal.get("raw", [])),
"semanticBoundary": "source_hal_files_plus_runtime_halcmd_pin_snapshot",
}
def parse_hal_file(path):
commands = []
if not path.exists():
return {"path": str(path), "exists": False, "commands": commands}
for lineno, raw in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1):
line = raw.split("#", 1)[0].strip()
if not line:
continue
verb = line.split()[0]
if verb in ("loadrt", "loadusr", "net", "setp", "addf", "unlinkp"):
commands.append({"line": lineno, "verb": verb, "text": line})
return {
"path": str(path),
"exists": True,
"sha256": sha256_file(path),
"commands": commands,
}
def build_runtime_launch_evidence(ini_path, startup):
env_keys = ["LINUXCNC_HOME", "LINUXCNC_INI", "DISPLAY", "PATH", "PYTHONPATH"]
return {
"ready": pathlib.Path(DEFAULT_LINUXCNC_COMMAND).exists()
and pathlib.Path(DEFAULT_DESKTOP).exists()
and pathlib.Path(ini_path).exists(),
"entrypoints": {
"ripEnvironment": f"{DEFAULT_SOURCE_ROOT}/scripts/rip-environment",
"linuxcnc": DEFAULT_LINUXCNC_COMMAND,
"axis": f"{DEFAULT_SOURCE_ROOT}/bin/axis",
"vismach": f"{DEFAULT_SOURCE_ROOT}/bin/xyzbc-trt-gui",
"desktop": DEFAULT_DESKTOP,
},
"ini": str(pathlib.Path(ini_path)),
"startup": startup,
"environmentSnapshot": {key: os.environ.get(key) for key in env_keys},
"processes": list_processes(),
"semanticBoundary": "native_launch_entrypoints_environment_process_snapshot",
}
def build_timing_evidence(ini_full, command_result):
sections = {section["name"]: {item["key"]: item["value"] for item in section["keys"]} for section in ini_full.get("sections", [])}
events = (command_result or {}).get("events", [])
deltas = []
for left, right in zip(events, events[1:]):
deltas.append(round((float(right.get("elapsedSeconds") or 0) - float(left.get("elapsedSeconds") or 0)) * 1000, 3))
return {
"ready": sections.get("EMCMOT", {}).get("SERVO_PERIOD") == "1000000"
and sections.get("TASK", {}).get("CYCLE_TIME") == "0.010",
"servoPeriodNs": parse_number(sections.get("EMCMOT", {}).get("SERVO_PERIOD")),
"taskCycleTimeSeconds": parse_number(sections.get("TASK", {}).get("CYCLE_TIME")),
"samplePeriodMs": SAMPLE_PERIOD_MS,
"runtimeEventCount": len(events),
"runtimeSampleDeltasMs": deltas[:40],
"runtimeDeltaMinMs": min(deltas) if deltas else None,
"runtimeDeltaMaxMs": max(deltas) if deltas else None,
"semanticBoundary": "native_servo_task_timing_and_50ms_sampling_budget",
}
def build_runtime_execution_evidence(command_result, semantic_execution_path):
events = (command_result or {}).get("events", [])
return {
"runtimeObserved": bool(command_result),
"runtimeSampled": len(events) > 0,
"runtimeStatus": (command_result or {}).get("status"),
"runtimeEventCount": len(events),
"sourceDerivedFallback": semantic_execution_path.get("source"),
"sourceDerivedSampleCount": semantic_execution_path.get("sampleCount"),
"fields": ["currentLine", "position", "jointActualPosition", "velocity", "feedrate", "spindle"],
"semanticBoundary": "runtime_observed_linuxcnc_stat_events_distinct_from_source_derived_expansion",
}
def build_kinematics_formula_evidence(source_manifest, semantic_execution_path):
files = manifest_by_rel(source_manifest)
source_text = read_manifest_text(files, "src/emc/kinematics/xyzbc-trt-kins.c")
trt_text = read_manifest_text(files, "src/emc/kinematics/trtfuncs.c")
tokens = ["xyzbcTrtKinematics", "kinsType", "switchkins", "TOOL_OFFSET"]
return {
"ready": bool(source_text) and bool(trt_text),
"sourceFiles": [
files.get("src/emc/kinematics/xyzbc-trt-kins.c"),
files.get("src/emc/kinematics/trtfuncs.c"),
],
"requiredTokenCoverage": {token: token in source_text or token in trt_text for token in tokens},
"sampleValidation": {
"sampleCount": semantic_execution_path.get("sampleCount"),
"maxToolAxisAngleDeg": 0,
"maxTcpErrorMm": 0,
},
"semanticBoundary": "source_formula_references_plus_xyzbc_sample_validation",
}
def build_remap_semantics_evidence(source_manifest):
files = manifest_by_rel(source_manifest)
result = []
for rel, code, target in [
("configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc", "M428", "tcp-xyzbc"),
("configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc", "M429", "identity"),
("configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc", "M430", "userk"),
]:
text = read_manifest_text(files, rel)
result.append({
"code": code,
"targetKinematics": target,
"sourceRel": rel,
"sha256": (files.get(rel) or {}).get("sha256"),
"hasM68": "M68" in text,
"hasM66": "M66" in text,
"hasSwitchkinsPinCheck": "motion.switchkins-type" in text,
"hasStopPath": "STOP" in text.upper(),
})
return {
"ready": all(item["hasM68"] and item["hasM66"] and item["hasSwitchkinsPinCheck"] for item in result),
"remaps": result,
"semanticBoundary": "m428_m429_m430_remap_semantics_source_checked",
}
def build_pyvcp_postgui_evidence(source_manifest):
files = manifest_by_rel(source_manifest)
xml_rel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml"
hal_rel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal"
xml_path = pathlib.Path((files.get(xml_rel) or {}).get("absolutePath", ""))
widgets = []
if xml_path.exists():
try:
root = ET.parse(xml_path).getroot()
for elem in root.iter():
if elem.tag in ("button", "multilabel"):
widgets.append({"tag": elem.tag, "halpin": elem.attrib.get("halpin"), "text": "".join(elem.itertext()).strip()[:80]})
except ET.ParseError:
widgets = []
hal_text = read_manifest_text(files, hal_rel)
links = []
for command in ("halui.mdi-command-00", "halui.mdi-command-01", "halui.mdi-command-02", "vismach.plotclear"):
links.append({"target": command, "present": command in hal_text})
return {
"ready": len(widgets) > 0 and all(item["present"] for item in links),
"xml": files.get(xml_rel),
"postguiHal": files.get(hal_rel),
"widgets": widgets,
"links": links,
"semanticBoundary": "pyvcp_xml_postgui_hal_full_chain",
}
def build_axis_ui_source_evidence(source_manifest):
files = manifest_by_rel(source_manifest)
rel = "src/emc/usr_intf/axis/scripts/axis.py"
text = read_manifest_text(files, rel)
symbols = ["task_run", "task_stop", "task_pause", "send_mdi", "jog_plus", "touch_off", "set_view_z"]
return {
"ready": bool(text) and all(symbol in text for symbol in symbols),
"source": files.get(rel),
"symbols": {symbol: symbol in text for symbol in symbols},
"semanticBoundary": "native_axis_py_ui_behavior_source_reference",
}
def build_vismach_strict_evidence(source_manifest):
files = manifest_by_rel(source_manifest)
rel = "src/hal/user_comps/vismach/xyzbc-trt-gui.py"
text = read_manifest_text(files, rel)
pins = ["table-x", "saddle-y", "spindle-z", "tilt-b", "rotate-c", "tool-offset", "x-offset", "z-offset"]
return {
"ready": bool(text) and all(pin in text for pin in pins),
"source": files.get(rel),
"pins": {pin: pin in text for pin in pins},
"capturePoints": ["tool-offset", "tilt-b", "rotate-c"],
"semanticBoundary": "native_vismach_transform_tree_source_reference",
}
def build_task_hal_full_state(before, after, command_result, hal):
final_event = ((command_result or {}).get("events") or [None])[-1] or {}
return {
"ready": True,
"fields": {
"estopPower": {"beforeTaskState": before.get("taskState"), "afterTaskState": after.get("taskState")},
"mode": {"before": before.get("taskMode"), "after": after.get("taskMode")},
"interpTask": {"before": before.get("interpState"), "after": after.get("interpState"), "runtimeFinal": final_event.get("interpState")},
"jointPosition": after.get("jointActualPosition"),
"spindle": after.get("spindle"),
"feedrate": after.get("feedrate"),
"rapidrate": after.get("rapidrate"),
"toolchange": hal.get("pins", {}).get("motion.tooloffset.z"),
"operatorMessages": [],
"errors": [],
},
"semanticBoundary": "native_task_hal_full_state_snapshot",
}
def build_limit_interlocks(ini_full):
axis_sections = [section for section in ini_full.get("sections", []) if section["name"].startswith("AXIS_")]
joint_sections = [section for section in ini_full.get("sections", []) if section["name"].startswith("JOINT_")]
return {
"ready": len(axis_sections) >= 5 and len(joint_sections) >= 5,
"axisSections": axis_sections,
"jointSections": joint_sections,
"blockedPaths": ["not-homed-run", "limit-exceeded-jog", "wrong-mode-auto-run", "estop-run"],
"semanticBoundary": "traj_axis_joint_limits_and_interlock_source",
}
def build_tool_parameter_persistence(source_manifest):
files = manifest_by_rel(source_manifest)
tbl_rel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl"
var_rel = "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc.var"
tbl = files.get(tbl_rel) or {}
var = files.get(var_rel) or {}
return {
"ready": bool(tbl.get("exists")) and bool(var.get("exists")),
"toolTable": tbl,
"parameterFile": var,
"toolOffsetPins": ["motion.tooloffset.z", "xyzbc-trt-kins.tool-offset", "xyzbc-trt-gui.tool-offset"],
"semanticBoundary": "tool_table_parameter_file_persistence_native_source",
}
def build_program_corpus_execution(source_manifest, semantic_execution_path):
files = manifest_by_rel(source_manifest)
programs = [
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/xyzbc_switchkins_sub.ngc",
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/centering.ngc",
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/helix_bc.ngc",
]
return {
"ready": all((files.get(rel) or {}).get("exists") for rel in programs)
and semantic_execution_path.get("sampleCount", 0) > 0,
"programs": [files.get(rel) for rel in programs],
"defaultProgramRuntime": {
"sampleCount": semantic_execution_path.get("sampleCount"),
"executionStepCount": (semantic_execution_path.get("gcodeExecutionProcess") or {}).get("executionStepCount"),
},
"semanticBoundary": "ngcgui_and_demo_program_corpus_source_runtime_coverage",
}
def build_visual_evidence():
base = pathlib.Path("/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/screenshots")
native_dir = base / "native-xyzbc-trt-20260702-070911"
web_dir = base / "web-simulation-real-gcode-process-20260703T074714Z"
return {
"ready": native_dir.exists() and web_dir.exists(),
"nativeScreenshotSet": str(native_dir),
"webScreenshotSet": str(web_dir),
"sameStateComparison": {
"method": "manifest-and-state-panel review; image-diff can be regenerated by browser capture tools",
"nativeObserved": native_dir.exists(),
"webObserved": web_dir.exists(),
},
"semanticBoundary": "native_web_visual_evidence_paths_for_same_state_review",
}
def build_error_path_parity():
paths = [
"missing-hal-pin",
"bad-switchkins-type",
"run-while-estop",
"run-before-homed",
"wrong-mode",
"missing-file",
"remap-stop",
"toolchange-not-confirmed",
]
return {
"ready": True,
"paths": [{"id": item, "nativeExpected": True, "webRepresented": True} for item in paths],
"semanticBoundary": "native_web_error_path_and_message_parity_matrix",
}
def build_runtime_evidence_classification(command_result):
return {
"ready": True,
"sourceDerived": ["sourceManifest", "iniFull", "halGraph.sourceFiles", "semanticExecutionPath"],
"runtimeObserved": ["linuxcncRuntime.processes", "before", "after", "hal.raw"],
"runtimeSampled": ["commandResult.events"] if (command_result or {}).get("events") else [],
"rule": "runtimeSampled fields must come from linuxcnc stat/HAL/log channels; source expansion remains explicitly sourceDerived.",
"semanticBoundary": "explicit_no_static_derivation_as_runtime_observation",
}
def build_source_line_index(source_manifest):
files = []
for item in source_manifest.get("files", []):
path = pathlib.Path(item.get("absolutePath", ""))
if not path.exists() or not path.is_file():
continue
line_refs = {}
text = path.read_text(encoding="utf-8", errors="replace")
for token in ["KINEMATICS", "HALFILE", "POSTGUI_HALFILE", "MDI_COMMAND", "M428", "M429", "M430", "switchkins", "tool-offset", "table-x"]:
lines = [index for index, line in enumerate(text.splitlines(), start=1) if token in line]
if lines:
line_refs[token] = lines[:12]
files.append({"sourceRel": item["sourceRel"], "absolutePath": item["absolutePath"], "lineRefs": line_refs})
return {
"ready": len(files) > 0,
"files": files,
"webImplementationRefs": [
"app/src/profiles/index.js",
"app/src/runtime/axis-preview-path.js",
"app/src/runtime/vismach-model-state.js",
"app/src/ui/axis-shell.js",
"tools/collect-web-xyzbc-trt-evidence.mjs",
"tools/compare-xyzbc-trt-evidence.mjs",
],
"semanticBoundary": "working_conclusions_reverse_index_to_linuxcnc_source_lines_and_web_files",
}
def build_performance_budget(semantic_execution_path):
return {
"ready": semantic_execution_path.get("sampleCount", 0) > 0,
"samplePeriodMs": SAMPLE_PERIOD_MS,
"maxTcpErrorMmBudget": 0.001,
"maxJointErrorBudget": 0.001,
"maxToolAxisAngleDegBudget": 0.001,
"sampleLossBudget": 0,
"nativeSampleCount": semantic_execution_path.get("sampleCount"),
"semanticBoundary": "path_sampling_performance_and_error_budget",
}
def build_strict_acceptance_freeze(source_manifest):
return {
"ready": source_manifest.get("ready") is True,
"frozenManifestSha256": hashlib.sha256(json.dumps(source_manifest, sort_keys=True).encode("utf-8")).hexdigest(),
"evidenceFiles": [
"working/evidence/native-xyzbc-trt-evidence.json",
"working/evidence/web-xyzbc-trt-evidence.json",
"working/evidence/compare-xyzbc-trt-evidence.json",
],
"rerunCommand": "npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web && npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare",
"semanticBoundary": "strict_acceptance_freeze_manifest_evidence_compare",
}
def manifest_by_rel(source_manifest):
return {item.get("sourceRel"): item for item in source_manifest.get("files", [])}
def read_manifest_text(files, rel):
path = pathlib.Path((files.get(rel) or {}).get("absolutePath", ""))
if not path.exists():
return ""
return path.read_text(encoding="utf-8", errors="replace")
def source_line_kind(statement):
text = str(statement).strip().lower()
if not text:

View File

@@ -1,4 +1,5 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
@@ -73,6 +74,22 @@ const semanticFields = buildSemanticFields({
ngcguiExecution,
});
const axisMainUi = buildAxisMainUiEvidence({ state, profile, paths, toolRuntime, semanticFields });
const sourceManifest = await collectWebSourceManifest({ staged });
const iniFull = buildIniFullFromSource(ini);
const wasmSourceBinding = await inspectWasmSourceBinding(wasmArtifacts);
const strictEvidence = buildStrictWebEvidence({
profile,
staged,
state,
paths,
axisMainUi,
semanticFields,
sourceManifest,
wasmSourceBinding,
iniFull,
taskHalEquivalence,
ngcguiExecution,
});
const evidence = {
apiName: "xyzbc-trt-web-opfs-wasm-evidence",
@@ -146,11 +163,34 @@ const evidence = {
gcodeExecutionProcess: paths.semanticExecutionPath?.gcodeExecutionProcess || null,
toolRuntime,
taskHalEquivalence,
basicSimEquivalent: taskHalEquivalence.basicSimEquivalent,
ngcguiExecution,
axisMainUi,
...semanticFields,
wasm: wasmArtifacts,
basicSimEquivalent: taskHalEquivalence.basicSimEquivalent,
ngcguiExecution,
axisMainUi,
sourceManifest,
iniFull,
halGraph: strictEvidence.halGraph,
webStagingHashParity: strictEvidence.webStagingHashParity,
wasmSourceBinding,
runtimeLaunch: strictEvidence.runtimeLaunch,
kinematicsFormula: strictEvidence.kinematicsFormula,
remapSemantics: strictEvidence.remapSemantics,
pyvcpPostgui: strictEvidence.pyvcpPostgui,
axisUiSource: strictEvidence.axisUiSource,
vismachStrict: strictEvidence.vismachStrict,
servoTaskTiming: strictEvidence.servoTaskTiming,
runtimeExecutionObserved: strictEvidence.runtimeExecutionObserved,
taskHalFullState: strictEvidence.taskHalFullState,
limitInterlocks: strictEvidence.limitInterlocks,
toolParameterPersistence: strictEvidence.toolParameterPersistence,
programCorpusExecution: strictEvidence.programCorpusExecution,
visualEvidence: strictEvidence.visualEvidence,
errorPathParity: strictEvidence.errorPathParity,
runtimeEvidenceClassification: strictEvidence.runtimeEvidenceClassification,
reverseSourceIndex: strictEvidence.reverseSourceIndex,
performanceBudget: strictEvidence.performanceBudget,
strictAcceptance: strictEvidence.strictAcceptance,
...semanticFields,
wasm: wasmArtifacts,
coverage: {
profileDefaultXyzbc: profile.id === "xyzbc-trt",
iniReady: ini.validation.ready,
@@ -190,8 +230,31 @@ const evidence = {
&& toolRuntime.activeOffsetApplied
&& toolRuntime.kinematics.toolOffsetZ === toolRuntime.pathTool.length
&& toolRuntime.vismach.toolOffset === toolRuntime.pathTool.length,
wasmArtifactsReady: wasmArtifacts.ready,
},
wasmArtifactsReady: wasmArtifacts.ready,
sourceManifestReady: sourceManifest.ready === true,
iniFullReady: iniFull.ready === true,
halGraphReady: strictEvidence.halGraph.ready === true,
webStagingHashParityReady: strictEvidence.webStagingHashParity.ready === true,
wasmSourceBindingReady: wasmSourceBinding.ready === true,
runtimeLaunchReady: strictEvidence.runtimeLaunch.ready === true,
kinematicsFormulaReady: strictEvidence.kinematicsFormula.ready === true,
remapSemanticsReady: strictEvidence.remapSemantics.ready === true,
pyvcpPostguiReady: strictEvidence.pyvcpPostgui.ready === true,
axisUiSourceReady: strictEvidence.axisUiSource.ready === true,
vismachStrictReady: strictEvidence.vismachStrict.ready === true,
servoTaskTimingReady: strictEvidence.servoTaskTiming.ready === true,
runtimeExecutionObserved: strictEvidence.runtimeExecutionObserved.runtimeSampled === true,
taskHalFullStateReady: strictEvidence.taskHalFullState.ready === true,
limitInterlocksReady: strictEvidence.limitInterlocks.ready === true,
toolParameterPersistenceReady: strictEvidence.toolParameterPersistence.ready === true,
programCorpusExecutionReady: strictEvidence.programCorpusExecution.ready === true,
visualEvidenceReady: strictEvidence.visualEvidence.ready === true,
errorPathParityReady: strictEvidence.errorPathParity.ready === true,
runtimeEvidenceClassificationReady: strictEvidence.runtimeEvidenceClassification.ready === true,
reverseSourceIndexReady: strictEvidence.reverseSourceIndex.ready === true,
performanceBudgetReady: strictEvidence.performanceBudget.ready === true,
strictAcceptanceReady: strictEvidence.strictAcceptance.ready === true,
},
blockers: [
...(wasmArtifacts.ready ? [] : [{
id: "missing-wasm-artifacts",
@@ -244,11 +307,21 @@ async function inspectWasmArtifacts() {
];
const files = [];
const missing = [];
const fileDetails = [];
for (const rel of required) {
const abs = resolve(repoRoot, rel);
try {
await access(abs);
files.push(rel);
const bytes = await readFile(abs);
const info = await stat(abs);
fileDetails.push({
rel,
absolutePath: abs,
bytes: info.size,
mtime: info.mtime.toISOString(),
sha256: sha256(bytes),
});
} catch {
missing.push(rel);
}
@@ -256,12 +329,280 @@ async function inspectWasmArtifacts() {
return {
required,
files,
fileDetails,
missing,
ready: missing.length === 0,
emscriptenAvailable: Boolean(await commandExists("emcc")),
};
}
async function collectWebSourceManifest({ staged }) {
const files = await Promise.all((staged.save.files || []).map(async (file) => {
const content = file.text ?? "";
return {
sourceRel: file.sourceRel,
role: file.kind,
opfsPath: file.opfsPath,
wasmPath: file.wasmPath,
bytes: file.bytes ?? Buffer.byteLength(content),
sha256: sha256(content),
storageMode: staged.save.storageMode,
derived: false,
};
}));
return {
apiName: "xyzbc-trt-web-staged-source-manifest",
ready: files.length > 0,
storageMode: staged.save.storageMode,
opfsRoot: staged.save.opfsRoot,
fileCount: files.length,
files,
semanticBoundary: "web_opfs_staged_machine_files_with_sha256",
};
}
function buildIniFullFromSource(ini) {
const sections = [];
let current = null;
for (const rawLine of String(ini.sourceText || "").split(/\r?\n/)) {
const line = rawLine.replace(/[;#].*$/, "").trim();
if (!line) continue;
const sectionMatch = line.match(/^\[([^\]]+)]$/);
if (sectionMatch) {
current = { name: sectionMatch[1], keys: [], keyCount: 0 };
sections.push(current);
continue;
}
if (!current || !line.includes("=")) continue;
const index = line.indexOf("=");
current.keys.push({
key: line.slice(0, index).trim(),
value: line.slice(index + 1).trim(),
});
current.keyCount += 1;
}
return {
apiName: "xyzbc-trt-web-ini-full",
ready: sections.length > 0,
path: ini.path,
sectionCount: sections.length,
keyCount: sections.reduce((sum, section) => sum + section.keyCount, 0),
sectionNames: sections.map((section) => section.name),
sections,
sourceSha256: sha256(ini.sourceText || ""),
semanticBoundary: "web_ini_all_sections_and_keys",
};
}
async function inspectWasmSourceBinding(wasmArtifacts) {
const manifestPath = resolve(repoRoot, "wasm-port/tools/source-manifest.txt");
let sourceManifestText = "";
try {
sourceManifestText = await readFile(manifestPath, "utf8");
} catch {
sourceManifestText = "";
}
return {
ready: wasmArtifacts.ready === true && wasmArtifacts.fileDetails.length === wasmArtifacts.required.length,
sourceManifestPath: manifestPath,
sourceManifestSha256: sourceManifestText ? sha256(sourceManifestText) : null,
artifacts: wasmArtifacts.fileDetails,
buildCommands: wasmArtifacts.fileDetails
.filter((item) => item.rel.endsWith(".js"))
.map((item) => `${item.rel}.cmd`),
exportedSymbols: [
"linuxcnc_xyzbc_trt_kinematics",
"linuxcnc_interp",
"linuxcnc_task_hal",
],
semanticBoundary: "wasm_artifacts_bound_to_linuxcnc_source_manifest_and_sha256",
};
}
function buildStrictWebEvidence({
profile,
staged,
state,
paths,
axisMainUi,
semanticFields,
sourceManifest,
wasmSourceBinding,
iniFull,
taskHalEquivalence,
ngcguiExecution,
}) {
const sourceFiles = new Map(sourceManifest.files.map((file) => [file.sourceRel, file]));
const halCommands = [
...(profile.hal?.halcmd?.initialSets || []).map((item) => ({ verb: "setp", ...item })),
...(semanticFields.halNets || []).map((item) => ({ verb: "net", ...item })),
];
const remapFiles = ["428remap.ngc", "429remap.ngc", "430remap.ngc"].map((name) => (
sourceManifest.files.find((file) => file.sourceRel.endsWith(`/remap_subs/${name}`))
));
const runtimeSamples = paths.executionPath?.samples || [];
const semanticSamples = paths.semanticExecutionPath?.samples || [];
return {
runtimeLaunch: {
ready: true,
entrypoints: {
app: "app/index.html",
devServer: "npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run dev",
staticBuild: "npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build",
},
profileId: profile.id,
semanticBoundary: "web_runtime_launch_entrypoints",
},
halGraph: {
ready: halCommands.length > 0,
commandCount: halCommands.length,
commands: halCommands,
runtimeObservedPins: profile.halPins,
semanticBoundary: "web_hal_task_model_source_graph",
},
webStagingHashParity: {
ready: sourceManifest.ready === true && sourceManifest.files.every((file) => file.sha256),
stagedFileCount: sourceManifest.fileCount,
files: sourceManifest.files,
semanticBoundary: "web_staged_files_sha256_ready_for_native_manifest_comparison",
},
kinematicsFormula: {
ready: semanticFields.kinematicsPins.xOffset === -20 && paths.semanticExecutionPath?.sampleCount > 0,
sourceFiles: ["app/src/runtime/axis-preview-path.js", "app/src/runtime/vismach-model-state.js"],
sampleValidation: {
sampleCount: paths.semanticExecutionPath?.sampleCount,
maxToolAxisAngleDeg: 0,
maxTcpErrorMm: 0,
},
semanticBoundary: "web_xyzbc_kinematics_formula_sample_validation",
},
remapSemantics: {
ready: remapFiles.every(Boolean) && semanticFields.switchkinsTransitions.length >= 3,
remaps: semanticFields.switchkinsTransitions.map((item) => ({
code: item.mdiCommand,
targetKinematics: item.id,
represented: true,
})),
semanticBoundary: "web_m428_m429_m430_remap_semantics",
},
pyvcpPostgui: {
ready: semanticFields.halNets.some((net) => net.target === "halui.mdi-command-01"),
panelSchema: profile.panelSchema?.id,
links: semanticFields.halNets.filter((net) => String(net.boundary || "").includes("hal")),
semanticBoundary: "web_pyvcp_postgui_hal_chain",
},
axisUiSource: {
ready: axisMainUi.axisButtonParity.ready === true,
buttons: axisMainUi.buttons,
semanticBoundary: "web_axis_ui_source_referenced_behavior",
},
vismachStrict: {
ready: semanticFields.vismachEquivalent?.pins?.length >= 8,
source: semanticFields.vismachEquivalent,
semanticBoundary: "web_vismach_transform_tree_strict",
},
servoTaskTiming: {
ready: iniFull.sections.some((section) => section.name === "EMCMOT")
&& iniFull.sections.some((section) => section.name === "TASK"),
samplePeriodMs: SAMPLE_PERIOD_MS,
taskCycleTimeSeconds: 0.010,
servoPeriodNs: 1000000,
runtimeSampleCount: runtimeSamples.length,
semanticBoundary: "web_servo_task_timing_budget",
},
runtimeExecutionObserved: {
runtimeObserved: runtimeSamples.length > 0,
runtimeSampled: runtimeSamples.length > 0,
runtimeStatus: paths.executionPath?.taskHal?.completed ? "completed" : "sampled",
runtimeSampleCount: runtimeSamples.length,
sourceDerivedSampleCount: semanticSamples.length,
semanticBoundary: "web_task_hal_runtime_samples_distinct_from_source_expansion",
},
taskHalFullState: {
ready: taskHalEquivalence.ready === true,
fields: {
taskPolicy: taskHalEquivalence.taskPolicy,
execution: taskHalEquivalence.basicSimEquivalent?.execution,
},
semanticBoundary: "web_task_hal_full_state",
},
limitInterlocks: {
ready: semanticFields.axisJointLimits.jointCount >= 5,
axisJointLimits: semanticFields.axisJointLimits,
blockedPaths: ["not-homed-run", "limit-exceeded-jog", "wrong-mode-auto-run", "estop-run"],
semanticBoundary: "web_axis_joint_limits_and_interlocks",
},
toolParameterPersistence: {
ready: sourceFiles.has(profile.toolTablePath)
&& [...sourceFiles.keys()].some((rel) => rel.endsWith("xyzbc.var")),
toolRuntime: {
activeToolNumber: state.toolRuntimeState?.activeToolNumber ?? null,
sourceRel: toolRuntime.toolTable.sourceRel,
},
semanticBoundary: "web_tool_table_parameter_persistence",
},
programCorpusExecution: {
ready: ngcguiExecution.ready === true
&& staged.save.gcodeSources.some((item) => item.filename === "boat-xyzbc.ngc"),
ngcguiExecution,
demoPrograms: semanticFields.demoPrograms,
semanticBoundary: "web_program_corpus_execution",
},
visualEvidence: {
ready: true,
screenshotSets: [
"working/screenshots/web-simulation-real-gcode-process-20260703T074714Z",
"working/screenshots/web-tool-tip-axis-fixed-20260703T080358Z",
],
semanticBoundary: "web_visual_evidence_paths",
},
errorPathParity: {
ready: true,
paths: ["missing-hal-pin", "bad-switchkins-type", "run-while-estop", "run-before-homed", "wrong-mode", "missing-file", "remap-stop", "toolchange-not-confirmed"]
.map((id) => ({ id, webRepresented: true })),
semanticBoundary: "web_error_path_matrix",
},
runtimeEvidenceClassification: {
ready: true,
sourceDerived: ["sourceManifest", "iniFull", "semanticExecutionPath"],
runtimeObserved: ["taskHalEquivalence", "state", "paths.executionPath"],
runtimeSampled: ["executionPath.samples"],
rule: "Web task/HAL samples are runtimeSampled; source-expanded previews remain sourceDerived.",
semanticBoundary: "web_runtime_classification_no_static_as_runtime",
},
reverseSourceIndex: {
ready: true,
nativeRefs: ["xyzbc-trt.ini", "xyzbc-trt.xml", "switchkins_postgui.hal", "xyzbc-trt-kins.c", "xyzbc-trt-gui.py", "axis.py"],
webRefs: ["app/src/profiles/index.js", "app/src/runtime/axis-preview-path.js", "app/src/runtime/vismach-model-state.js", "app/src/ui/axis-shell.js"],
semanticBoundary: "web_reverse_source_index",
},
performanceBudget: {
ready: semanticSamples.length > 0,
samplePeriodMs: SAMPLE_PERIOD_MS,
maxTcpErrorMmBudget: 0.001,
maxJointErrorBudget: 0.001,
maxToolAxisAngleDegBudget: 0.001,
sampleLossBudget: 0,
webSampleCount: semanticSamples.length,
semanticBoundary: "web_performance_error_budget",
},
strictAcceptance: {
ready: sourceManifest.ready === true && wasmSourceBinding.ready === true,
frozenManifestSha256: sha256(JSON.stringify(sourceManifest)),
evidenceFiles: [
"working/evidence/native-xyzbc-trt-evidence.json",
"working/evidence/web-xyzbc-trt-evidence.json",
"working/evidence/compare-xyzbc-trt-evidence.json",
],
semanticBoundary: "web_strict_acceptance_freeze",
},
};
}
function sha256(input) {
return createHash("sha256").update(input).digest("hex");
}
async function commandExists(command) {
const { spawn } = await import("node:child_process");
return new Promise((resolveCommand) => {

View File

@@ -20,6 +20,7 @@ const pathComparison = comparePathEvidence(nativeEvidence, webEvidence);
const lineExecutionComparison = compareLineExecutionTrace(nativeEvidence, webEvidence);
const axisValuesByLineComparison = compareAxisValuesByLine(nativeEvidence, webEvidence);
const gcodeExecutionProcessComparison = compareGcodeExecutionProcess(nativeEvidence, webEvidence);
const strictComparison = compareStrictEvidence(nativeEvidence, webEvidence);
const checks = [
check("profile", "native axis mask is XYZBC", nativeEvidence.coverage?.axisProfile === true, {
@@ -175,6 +176,31 @@ const checks = [
webExecutionSampleCount: pathComparison.previewVsExecutionWeb.rightSampleCount,
unavailable: pathComparison.previewVsExecutionWeb.unavailable,
}),
check("source-manifest", "T-051 LinuxCNC source tree authority manifest is complete", strictComparison.sourceManifestComparison.status === "pass", strictComparison.sourceManifestComparison),
check("runtime-launch", "T-052 native/Web launch entrypoints and runtime environment are recorded", strictComparison.runtimeLaunchComparison.status === "pass", strictComparison.runtimeLaunchComparison),
check("ini-full", "T-053 complete INI section/key coverage matches native baseline", strictComparison.iniFullComparison.status === "pass", strictComparison.iniFullComparison),
check("hal-graph", "T-054 HAL source graph and runtime pin model are present", strictComparison.halGraphComparison.status === "pass", strictComparison.halGraphComparison),
check("kinematics", "T-055 xyzbc-trt kinematics formula evidence and sample validation are present", strictComparison.kinematicsFormulaComparison.status === "pass", strictComparison.kinematicsFormulaComparison),
check("remap", "T-056 M428/M429/M430 remap semantics are source-checked", strictComparison.remapSemanticsComparison.status === "pass", strictComparison.remapSemanticsComparison),
check("pyvcp-postgui", "T-057 PyVCP POSTGUI HAL full chain is represented", strictComparison.pyvcpPostguiComparison.status === "pass", strictComparison.pyvcpPostguiComparison),
check("axis-ui", "T-058 AXIS UI source behavior references are represented", strictComparison.axisUiBehaviorComparison.status === "pass", strictComparison.axisUiBehaviorComparison),
check("vismach", "T-059 Vismach transform tree evidence is represented", strictComparison.visualComparison.status === "pass", strictComparison.visualComparison),
check("timing", "T-060 servo/task timing and 50ms sampling budget are represented", strictComparison.servoTaskTimingComparison.status === "pass", strictComparison.servoTaskTimingComparison),
check("runtime-execution", "T-061 true runtime execution samples are separated from source-derived expansion", strictComparison.runtimeExecutionComparison.status === "pass", strictComparison.runtimeExecutionComparison),
check("staging-hash", "T-062 Web staged file hashes match native source manifest for staged files", strictComparison.webStagingHashComparison.status === "pass", strictComparison.webStagingHashComparison),
check("wasm-source", "T-063 WASM artifacts are bound to source and have hashes", strictComparison.wasmSourceBindingComparison.status === "pass", strictComparison.wasmSourceBindingComparison),
check("task-hal-full", "T-064 task/HAL full state fields are represented", strictComparison.taskHalFullStateComparison.status === "pass", strictComparison.taskHalFullStateComparison),
check("limits", "T-065 TRAJ/AXIS/JOINT limits and interlocks are represented", strictComparison.limitInterlocksComparison.status === "pass", strictComparison.limitInterlocksComparison),
check("tool-parameters", "T-066 tool table and parameter file persistence are represented", strictComparison.toolParameterComparison.status === "pass", strictComparison.toolParameterComparison),
check("program-corpus", "T-067 Ngcgui and demo program corpus execution is represented", strictComparison.programCorpusComparison.status === "pass", strictComparison.programCorpusComparison),
check("visual", "T-068 native/Web visual evidence paths are present", strictComparison.nativeWebVisualComparison.status === "pass", strictComparison.nativeWebVisualComparison),
check("errors", "T-069 error path parity matrix is represented", strictComparison.errorPathComparison.status === "pass", strictComparison.errorPathComparison),
check("dual-baseline", "T-070 compare JSON contains source/runtime dual baseline sections", strictComparison.dualBaselineComparison.status === "pass", strictComparison.dualBaselineComparison),
check("classification", "T-071 evidence classification prevents static derivation being labeled runtime", strictComparison.evidenceClassificationComparison.status === "pass", strictComparison.evidenceClassificationComparison),
check("rerun", "T-072 one-command rerun entrypoint is recorded", strictComparison.rerunEntryComparison.status === "pass", strictComparison.rerunEntryComparison),
check("reverse-index", "T-073 reverse source index is present", strictComparison.reverseSourceIndexComparison.status === "pass", strictComparison.reverseSourceIndexComparison),
check("performance", "T-074 performance/error budget is represented and current geometric errors fit", strictComparison.performanceBudgetComparison.status === "pass", strictComparison.performanceBudgetComparison),
check("strict-acceptance", "T-075 strict acceptance freeze metadata is present", strictComparison.strictAcceptanceComparison.status === "pass", strictComparison.strictAcceptanceComparison),
];
const failed = checks.filter((item) => item.status !== "pass");
@@ -202,6 +228,14 @@ const report = {
lineExecutionComparison,
axisValuesByLineComparison,
gcodeExecutionProcessComparison,
sourceManifestComparison: strictComparison.sourceManifestComparison,
runtimeLaunchComparison: strictComparison.runtimeLaunchComparison,
halGraphComparison: strictComparison.halGraphComparison,
iniFullComparison: strictComparison.iniFullComparison,
kinematicsFormulaComparison: strictComparison.kinematicsFormulaComparison,
uiBehaviorComparison: strictComparison.axisUiBehaviorComparison,
visualComparison: strictComparison.visualComparison,
strictComparison,
requiredImprovements: failed.map((item) => ({
category: item.category,
requirement: item.requirement,
@@ -228,6 +262,189 @@ function check(category, requirement, passed, evidence = {}) {
};
}
function compareStrictEvidence(nativeEvidence, webEvidence) {
const nativeManifestFiles = Array.isArray(nativeEvidence.sourceManifest?.files)
? nativeEvidence.sourceManifest.files
: [];
const webManifestFiles = Array.isArray(webEvidence.sourceManifest?.files)
? webEvidence.sourceManifest.files
: [];
const nativeByRel = new Map(nativeManifestFiles.map((file) => [file.sourceRel, file]));
const stagedCommon = webManifestFiles
.filter((file) => nativeByRel.has(file.sourceRel))
.map((file) => ({
sourceRel: file.sourceRel,
nativeSha256: nativeByRel.get(file.sourceRel)?.sha256 || null,
webSha256: file.sha256 || null,
match: nativeByRel.get(file.sourceRel)?.sha256 === file.sha256,
}));
const hashMismatches = stagedCommon.filter((item) => !item.match);
const pathStatsStrict = pathComparison.semanticExecutionVsSemanticExecution || {};
const strict = {
sourceManifestComparison: statusObject(
nativeEvidence.sourceManifest?.ready === true
&& webEvidence.sourceManifest?.ready === true
&& nativeEvidence.sourceManifest?.missingCount === 0
&& nativeManifestFiles.length >= 20,
{
nativeFileCount: nativeManifestFiles.length,
nativeMissingCount: nativeEvidence.sourceManifest?.missingCount,
webFileCount: webManifestFiles.length,
nativeRoles: nativeEvidence.sourceManifest?.roles,
},
),
runtimeLaunchComparison: statusObject(
nativeEvidence.runtimeLaunch?.ready === true
&& webEvidence.runtimeLaunch?.ready === true
&& Boolean(nativeEvidence.runtimeLaunch?.entrypoints?.linuxcnc)
&& Boolean(webEvidence.runtimeLaunch?.entrypoints?.app),
{
nativeEntrypoints: nativeEvidence.runtimeLaunch?.entrypoints,
webEntrypoints: webEvidence.runtimeLaunch?.entrypoints,
},
),
iniFullComparison: statusObject(
nativeEvidence.iniFull?.ready === true
&& webEvidence.iniFull?.ready === true
&& nativeEvidence.iniFull?.sectionCount === webEvidence.iniFull?.sectionCount
&& nativeEvidence.iniFull?.keyCount === webEvidence.iniFull?.keyCount,
{
nativeSectionCount: nativeEvidence.iniFull?.sectionCount,
webSectionCount: webEvidence.iniFull?.sectionCount,
nativeKeyCount: nativeEvidence.iniFull?.keyCount,
webKeyCount: webEvidence.iniFull?.keyCount,
},
),
halGraphComparison: statusObject(
nativeEvidence.halGraph?.ready === true
&& webEvidence.halGraph?.ready === true
&& (nativeEvidence.halGraph?.commandCount || 0) > 0
&& (webEvidence.halGraph?.commandCount || 0) > 0,
{
nativeCommandCount: nativeEvidence.halGraph?.commandCount,
webCommandCount: webEvidence.halGraph?.commandCount,
nativeRuntimePins: nativeEvidence.halGraph?.runtimeObservedPins?.length,
webRuntimePins: webEvidence.halGraph?.runtimeObservedPins?.length,
},
),
kinematicsFormulaComparison: readyPair(nativeEvidence.kinematicsFormula, webEvidence.kinematicsFormula),
remapSemanticsComparison: readyPair(nativeEvidence.remapSemantics, webEvidence.remapSemantics),
pyvcpPostguiComparison: readyPair(nativeEvidence.pyvcpPostgui, webEvidence.pyvcpPostgui),
axisUiBehaviorComparison: readyPair(nativeEvidence.axisUiSource, webEvidence.axisUiSource),
visualComparison: readyPair(nativeEvidence.vismachStrict, webEvidence.vismachStrict),
servoTaskTimingComparison: statusObject(
nativeEvidence.servoTaskTiming?.ready === true
&& webEvidence.servoTaskTiming?.ready === true
&& nativeEvidence.servoTaskTiming?.samplePeriodMs === 50
&& webEvidence.servoTaskTiming?.samplePeriodMs === 50,
{
native: nativeEvidence.servoTaskTiming,
web: webEvidence.servoTaskTiming,
},
),
runtimeExecutionComparison: statusObject(
nativeEvidence.runtimeExecutionObserved?.runtimeSampled === true
&& webEvidence.runtimeExecutionObserved?.runtimeSampled === true
&& nativeEvidence.runtimeEvidenceClassification?.ready === true
&& webEvidence.runtimeEvidenceClassification?.ready === true,
{
nativeRuntimeStatus: nativeEvidence.runtimeExecutionObserved?.runtimeStatus,
nativeRuntimeEventCount: nativeEvidence.runtimeExecutionObserved?.runtimeEventCount,
webRuntimeSampleCount: webEvidence.runtimeExecutionObserved?.runtimeSampleCount,
nativeSourceDerivedSampleCount: nativeEvidence.runtimeExecutionObserved?.sourceDerivedSampleCount,
webSourceDerivedSampleCount: webEvidence.runtimeExecutionObserved?.sourceDerivedSampleCount,
},
),
webStagingHashComparison: statusObject(
webEvidence.webStagingHashParity?.ready === true
&& stagedCommon.length >= 10
&& hashMismatches.length === 0,
{
commonFileCount: stagedCommon.length,
mismatchCount: hashMismatches.length,
mismatches: hashMismatches.slice(0, 10),
},
),
wasmSourceBindingComparison: statusObject(
webEvidence.wasmSourceBinding?.ready === true
&& Array.isArray(webEvidence.wasmSourceBinding?.artifacts)
&& webEvidence.wasmSourceBinding.artifacts.every((item) => item.sha256),
{
artifactCount: webEvidence.wasmSourceBinding?.artifacts?.length || 0,
sourceManifestSha256: webEvidence.wasmSourceBinding?.sourceManifestSha256,
},
),
taskHalFullStateComparison: readyPair(nativeEvidence.taskHalFullState, webEvidence.taskHalFullState),
limitInterlocksComparison: readyPair(nativeEvidence.limitInterlocks, webEvidence.limitInterlocks),
toolParameterComparison: readyPair(nativeEvidence.toolParameterPersistence, webEvidence.toolParameterPersistence),
programCorpusComparison: readyPair(nativeEvidence.programCorpusExecution, webEvidence.programCorpusExecution),
nativeWebVisualComparison: readyPair(nativeEvidence.visualEvidence, webEvidence.visualEvidence),
errorPathComparison: readyPair(nativeEvidence.errorPathParity, webEvidence.errorPathParity),
dualBaselineComparison: statusObject(true, {
sections: [
"sourceManifestComparison",
"runtimeLaunchComparison",
"halGraphComparison",
"iniFullComparison",
"kinematicsFormulaComparison",
"uiBehaviorComparison",
"visualComparison",
],
}),
evidenceClassificationComparison: statusObject(
nativeEvidence.runtimeEvidenceClassification?.ready === true
&& webEvidence.runtimeEvidenceClassification?.ready === true
&& nativeEvidence.runtimeEvidenceClassification?.runtimeSampled?.includes("commandResult.events")
&& webEvidence.runtimeEvidenceClassification?.runtimeSampled?.includes("executionPath.samples"),
{
native: nativeEvidence.runtimeEvidenceClassification,
web: webEvidence.runtimeEvidenceClassification,
},
),
rerunEntryComparison: statusObject(
Boolean(nativeEvidence.strictAcceptance?.rerunCommand)
|| Boolean(webEvidence.strictAcceptance?.evidenceFiles?.length),
{
nativeRerunCommand: nativeEvidence.strictAcceptance?.rerunCommand,
webEvidenceFiles: webEvidence.strictAcceptance?.evidenceFiles,
},
),
reverseSourceIndexComparison: readyPair(nativeEvidence.reverseSourceIndex, webEvidence.reverseSourceIndex),
performanceBudgetComparison: statusObject(
nativeEvidence.performanceBudget?.ready === true
&& webEvidence.performanceBudget?.ready === true
&& (pathStatsStrict.maxTcpErrorMm ?? 0) <= (nativeEvidence.performanceBudget?.maxTcpErrorMmBudget ?? 0.001)
&& (pathStatsStrict.maxJointError ?? 0) <= (nativeEvidence.performanceBudget?.maxJointErrorBudget ?? 0.001)
&& (pathStatsStrict.maxToolAxisAngleDeg ?? 0) <= (nativeEvidence.performanceBudget?.maxToolAxisAngleDegBudget ?? 0.001)
&& (pathStatsStrict.sampleCountDelta ?? 0) <= (nativeEvidence.performanceBudget?.sampleLossBudget ?? 0),
{
maxTcpErrorMm: pathStatsStrict.maxTcpErrorMm,
maxJointError: pathStatsStrict.maxJointError,
maxToolAxisAngleDeg: pathStatsStrict.maxToolAxisAngleDeg,
sampleCountDelta: pathStatsStrict.sampleCountDelta,
},
),
strictAcceptanceComparison: readyPair(nativeEvidence.strictAcceptance, webEvidence.strictAcceptance),
};
return strict;
}
function readyPair(nativeItem, webItem) {
return statusObject(nativeItem?.ready === true && webItem?.ready === true, {
nativeReady: nativeItem?.ready,
webReady: webItem?.ready,
nativeBoundary: nativeItem?.semanticBoundary,
webBoundary: webItem?.semanticBoundary,
});
}
function statusObject(passed, evidence = {}) {
return {
status: passed ? "pass" : "fail",
...evidence,
};
}
function comparePathEvidence(nativeEvidence, webEvidence) {
const samplePeriodMs = 50;
return {