Implement WebAssembly runtime and kinematics coverage

This commit is contained in:
2026-08-21 06:04:48 +08:00
parent 5dd614c335
commit 554b72dae8
112 changed files with 47148 additions and 807 deletions

View File

@@ -1,8 +1,8 @@
# LinuxCNC Web/WASM # LinuxCNC Web/WASM
本仓库的目标是将 LinuxCNC `2.10.0~pre0` 的仿真行为移植到浏览器。当前提交完成的是实施前 M0/M1 骨架,不是 LinuxCNC 功能完成声明。 本仓库的目标是将 LinuxCNC `2.10.0~pre0` 的仿真行为移植到浏览器。当前已完成 posemath/TP、rs274ngc interpreter 和 fixed-tick motion queue 核心切片;这仍不是 LinuxCNC 全功能完成声明。
恢复或开始工作时,首先读取 [`nextTask.md`](nextTask.md)。它是当前任务节点、下一任务节点、验证证据和切换规则的唯一接续入口。 恢复或开始工作时,首先读取 [`nextTask.md`](nextTask.md)。它是唯一接续入口,刻意限制在 8 KiB 以内,不包含聊天记录、完整日志或历史证据。只有定位旧证据时才按需读取 [`docs/task-history.md`](docs/task-history.md) 和其中引用的产物。
## 当前状态 ## 当前状态
@@ -11,6 +11,10 @@
- 覆盖种子:`coverage/manifest.jsonl`。所有条目初始为 `planned/needs_owner`,没有来源会被静默忽略。 - 覆盖种子:`coverage/manifest.jsonl`。所有条目初始为 `planned/needs_owner`,没有来源会被静默忽略。
- WASM ABI 骨架:`wasm-core/include/sim_api.h`;当前实现只验证生命周期、固定 tick、快照和状态导入导出。 - WASM ABI 骨架:`wasm-core/include/sim_api.h`;当前实现只验证生命周期、固定 tick、快照和状态导入导出。
- 前端、Worker、HAL、解释器、TP、Python、OPFS 和 UI 功能仍须按文档里程碑实现。 - 前端、Worker、HAL、解释器、TP、Python、OPFS 和 UI 功能仍须按文档里程碑实现。
- `K0-INV-01`、`K0-INV-02`、`K0-ORACLE-01`、`K1-CONTRACT-01`、`K1-CONTRACT-02`、`K1-NUM-01`、`K2-TRIV-01`、`K2-TRIV-02`、`K2-COUPLED-01`、`K2-HAL-01`、`K3-5AXIS-01`、`K3-TRT-01`、`K3-ROBOT-01` 和 `K3-GENSER-01` 已完成;Genser melfa/puma560 DH、迭代 residual、固定上限和 native/WASM 对照见 `coverage/genser-kinematics.report.json`;其它运动学证据保持在对应 `coverage/*-kinematics.report.json`。旧 `M2-CORE-04` 因锁定来源缺少声明的 `linuxcnc/unit_tests/kinematics/**` 输入而保留为 blocked 证据。
- `K4-DELTA-01` 已完成 linear/rotary Delta 的统一解析 FK/IK、XYZ/extra-joint mapping、默认/alternate root、工作空间/限位/奇异边界和 native/WASM 对照;四个真实配置候选、缺失 HAL 资源和官方 oracle blocked 清单见 `coverage/delta-kinematics.report.json`。旧 `M2-CORE-04` 因锁定来源缺少声明的 `linuxcnc/unit_tests/kinematics/**` 输入而保留为 blocked 证据。
- `K4-TRIPOD-01` 已完成 point-vertex tripod 的统一 FK/IK/mapping;真实 `tripod.ini`/`tripodsim.hal`、XYZ/AD-BD-CD 映射、above/below 根分支、无解/基面奇异/限位/退化几何和 native/WASM state hash 对照见 `coverage/tripod-kinematics.report.json`。`nextTask.md` 当前进入 `K4-HEX-01`;官方 oracle 因锁定来源缺少 `linuxcnc/unit_tests/kinematics/**` 继续显式 blocked。
- `K4-HEX-01` 已完成 genhex 六腿 Stewart 平台的统一 FK/IK/mapping;真实 `hexapod.ini`/`kinematics.hal`、Newton 6x6 正解、闭式逆解、迭代/奇异/限位边界和 native/WASM 对照见 `coverage/genhex-kinematics.report.json`。`nextTask.md` 当前进入 `K4-PENTA-01`;官方 oracle 继续显式 blocked。
## 重建 M0 ## 重建 M0
@@ -18,8 +22,11 @@
python3 scripts/generate_source_inventory.py python3 scripts/generate_source_inventory.py
python3 scripts/validate_baseline.py python3 scripts/validate_baseline.py
python3 -m unittest discover -s scripts -p 'test_*.py' -v python3 -m unittest discover -s scripts -p 'test_*.py' -v
python3 scripts/validate_task_pointer.py
``` ```
模型请求也必须先经过预算门禁:用 `scripts/context_packet.py` 生成固定 profile 的 packet,再运行 `scripts/validate_model_budget.py packet.json --context-window <window> --overhead-tokens <current-request-overhead>`。覆盖值只能缩小 profile;请求外开销不得包含上一轮聊天。请求返回后用 `validate_provider_response` 校验 provider usage,流式响应交给 `scripts/model_stream.py` 消费;断流、上下文超限、usage 缺失或截断只写 metadata-only checkpoint,并从更小的 packet 重建,绝不把旧响应拼回上下文。
## 编译 ABI 骨架 ## 编译 ABI 骨架
需要 Emscripten。该命令同时运行原生 ABI 单测并生成 `build-wasm/linuxcnc-sim-core.js`/`.wasm`: 需要 Emscripten。该命令同时运行原生 ABI 单测并生成 `build-wasm/linuxcnc-sim-core.js`/`.wasm`:
@@ -28,6 +35,6 @@ python3 -m unittest discover -s scripts -p 'test_*.py' -v
scripts/build-wasm.sh scripts/build-wasm.sh
``` ```
完整路线、功能完整性和退出门禁见 [`docs/linuxcnc-web-wasm-implementation-readiness.md`](docs/linuxcnc-web-wasm-implementation-readiness.md)。专项设计仍分别位于 `docs/` 下的 roadmap、source map、coverage、communication、execution 和 playbook 文档。 完整路线、功能完整性和退出门禁见 [`docs/linuxcnc-web-wasm-implementation-readiness.md`](docs/linuxcnc-web-wasm-implementation-readiness.md)。面向“完整移植仿真机床”的启动闭环、运行时依赖、固定 tick、资源闭包和分阶段产物见 [`docs/linuxcnc-web-wasm-full-port-implementation.md`](docs/linuxcnc-web-wasm-full-port-implementation.md);运动学正逆解、机床类型分族、分支/奇异性和 K0-K8 主线见 [`docs/linuxcnc-web-wasm-kinematics-migration-plan.md`](docs/linuxcnc-web-wasm-kinematics-migration-plan.md)。专项设计仍分别位于 `docs/` 下的 roadmap、source map、coverage、communication、execution 和 playbook 文档。
依赖下载允许使用中国大陆镜像;请先阅读 `context-lock.json` 的 `downloads` 策略。镜像只改变传输位置,不能改变版本、文件字节或校验记录。 依赖下载允许使用中国大陆镜像;请先阅读 `context-lock.json` 的 `downloads` 策略。镜像只改变传输位置,不能改变版本、文件字节或校验记录。

View File

@@ -60,18 +60,25 @@
"runtime_pin_rule": "Changing mirror URL without changing bytes does not change context; changing bytes or version creates a new context_id." "runtime_pin_rule": "Changing mirror URL without changing bytes does not change context; changing bytes or version creates a new context_id."
}, },
"inventory_counts": { "inventory_counts": {
"total": 8020, "total": 8035,
"source_index": 8020, "source_index": 8035,
"manifest_seed": 8020, "manifest_seed": 8035,
"document_capabilities": 2868, "document_capabilities": 2868,
"source_symbols": 23983, "source_symbols": 23983,
"config_resources": 16093, "config_resources": 16093,
"test_entries": 321, "test_entries": 321,
"normalized_sources": 51285, "normalized_sources": 51285,
"dependency_edges": 37160 "dependency_edges": 37160,
"kinematics_source_files": 35,
"kinematics_config_entries": 264,
"kinematics_hal_entries": 113,
"kinematics_test_files": 6,
"kinematics_graph_entries": 377,
"kinematics_graph_edges": 2784,
"kinematics_oracle_samples": 11
}, },
"source_index_sha256": "843e35bb7b4c086453c4d370bbd2d1f20445b13a1a56f7273d4eb13079bae41d", "source_index_sha256": "b0719a9cf1031f7305e29075e645f21aa3c3630401e284825565a8b206c83ce4",
"manifest_seed_sha256": "1a64bedd3629e665411fb87abb6b5de94eed11456acab305fade6bdec18076fd", "manifest_seed_sha256": "70cd247690616ca260b111fb737c1277c5336f845acc5ac27160eaf87a7c9f93",
"document_capabilities_sha256": "fa5140bd4995224406381e4190e76bf81f64bd24dbf93a2fd10421c45aee4316", "document_capabilities_sha256": "fa5140bd4995224406381e4190e76bf81f64bd24dbf93a2fd10421c45aee4316",
"source_symbols_sha256": "5fc72e6a268d2220b6335f7f6834c925f13c5c35461fa399a400e6621958875a", "source_symbols_sha256": "5fc72e6a268d2220b6335f7f6834c925f13c5c35461fa399a400e6621958875a",
"config_resources_sha256": "d4d02cb13f2f9cb58404630d4de8ac0226e02075166fe30f2b03a6abca4be5bc", "config_resources_sha256": "d4d02cb13f2f9cb58404630d4de8ac0226e02075166fe30f2b03a6abca4be5bc",
@@ -81,8 +88,29 @@
"runtime_mapping_report_sha256": "f4be10828fda8201ff9e69de95bc68e5def42fc7206a851b37474c21b4da0a17", "runtime_mapping_report_sha256": "f4be10828fda8201ff9e69de95bc68e5def42fc7206a851b37474c21b4da0a17",
"python_runtime_manifest_sha256": "340faa6ef5bedcaeb0583f463e6b89cb442ddf1d83d7e6ee8d96cee438fe4213", "python_runtime_manifest_sha256": "340faa6ef5bedcaeb0583f463e6b89cb442ddf1d83d7e6ee8d96cee438fe4213",
"python_runtime_report_sha256": "be531c8cb5af55f9527b6b6b3955ff908a748fae0e37952fa699aecf1a44b399", "python_runtime_report_sha256": "be531c8cb5af55f9527b6b6b3955ff908a748fae0e37952fa699aecf1a44b399",
"hal_runtime_report_sha256": "70b99118ae6957456e592512d752aa137f59cae897ec546509ff26456879dd90", "hal_runtime_report_sha256": "85f79be090c5fceb3ee633da0fff57c908604938592c9b2cf6a1b2151b588e6b",
"posemath_runtime_report_sha256": "26b04fb285b5d085a980856fffe5707c2e2ccaeddc08c06f2da2491e76256a39", "posemath_runtime_report_sha256": "e9680e09dab67f85736f7f6a3bb6bb4456f5f4c93428f72ee3dc94ed0456face",
"interpreter_runtime_report_sha256": "1ffed00d9f0affbbc55e73ab4dca12b0446af11ea7861dfb7597ebe5e1a6cda2",
"motion_runtime_report_sha256": "fb2d36130bf3168bea9b13fe08cc2225d72190a7aa9c99a0f0db7f148ac171df",
"kinematics_runtime_report_sha256": "7090db32ac2af28e13c115b63ba7b894596efe25d8eec9fc27f097db931fde2c",
"kinematics_source_closure_report_sha256": "d3af5e19e38d8dc2640323dcf755d0323e871815087b9eece9d76544b0b0cfa6",
"kinematics_config_graph_report_sha256": "1c7ffe85730b23ee303beae95c7c565506e7c8f10c7bf68d23b980be22b46521",
"kinematics_oracle_samples_sha256": "df32ade49e3c3beb49e52d26200e37766dd04c5bee5dba4b1c87695439bf87bd",
"kinematics_oracle_report_sha256": "66c98703195c69f165a932aadc29015ab748aa005e8bfc223cb04d9d2f65a1af",
"kinematics_golden_schema_sha256": "8d6abe21ad350bb5a5ddc38d8fd9044ed2fa622f4331804a85cad55eb5a30a63",
"kinematics_abi_contract_sha256": "3177c4d28200c5136981d28489ac4578a84360f9688ee8614e23af8d7526b50b",
"kinematics_mapping_contract_sha256": "49bd93c716bd5b5eb6064f75ea2b8ba26dfa819c6166c22c5375ebeba927dd91",
"kinematics_numeric_policy_sha256": "b78c2b464771083e279689d33d65169e99ef01b08e347a0e75b7486d8d2d0134",
"trivkins_integration_report_sha256": "98c2a61c2fa3503782494768fcb7b0e5a13e998a519ce9b3629549194fa29ef8",
"coupled_kinematics_report_sha256": "696e62679095e831f86b24a34ae4daba69bcab81da47f92e7c4ee680dfcd70fd",
"corexy_hal_report_sha256": "e1e5c03f5d2dc30647b19a59a59ab625d440eb90a522d8e2366050761938064b",
"fiveaxis_kinematics_report_sha256": "5b02508761b7c1d7731e1bf4210563bb289fb8f9860f98ab1aa0a0d316f77369",
"trt_kinematics_report_sha256": "2f655742b5f9de09de903964a4cd85de0391d06ef1e2a35da0d6854f26649b51",
"robot_kinematics_report_sha256": "e3c27bc17b74206038af29fc8f5cc5bc39aca9af161b4f1d2de3be3f9905e5c3",
"genser_kinematics_report_sha256": "9c537fa9d25eb90e10cf594ac960ade29ac3c5dcc4b2a8e0cbef428c147e8e2c",
"delta_kinematics_report_sha256": "c0402979c5783292c9eed353e94c6b308b861457e8fb73a6e6e06882b81939a2",
"tripod_kinematics_report_sha256": "a17e63e50e255ab5ea5bcee6d78796e279f7b0e1c7f753a7963b2ce2b3a787ed",
"genhex_kinematics_report_sha256": "780043fabe9caca0c10554ac7d330fc21e67983177dfe72434dd616a85d8caac",
"dependency_closure_sha256": "39ab6b831f4eb53347dbb509855ca05bc557e25abc5d76837c45ce037a3e44a9", "dependency_closure_sha256": "39ab6b831f4eb53347dbb509855ca05bc557e25abc5d76837c45ce037a3e44a9",
"golden_schema_sha256": "5513535b9480d7100ef3ef59ac933c4040bd7198dc419693a7ee68098171c423", "golden_schema_sha256": "5513535b9480d7100ef3ef59ac933c4040bd7198dc419693a7ee68098171c423",
"native_runner_report_sha256": "6f2105f154d12b99f4e1cd4af3cdb5df4a462f3665f1447d5959384be5d4fae2", "native_runner_report_sha256": "6f2105f154d12b99f4e1cd4af3cdb5df4a462f3665f1447d5959384be5d4fae2",

View File

@@ -0,0 +1,134 @@
{
"component": {
"name": "corexy_by_hal",
"period": "fixed HAL tick",
"pins": [
"j0-motor-pos-cmd",
"j1-motor-pos-cmd",
"alpha-cmd",
"beta-cmd",
"alpha-fb",
"beta-fb",
"j0-motor-pos-fb",
"j1-motor-pos-fb"
],
"thread": "servo-thread"
},
"config_scan": {
"alpha_command_to_stepgen": true,
"beta_command_to_stepgen": true,
"component_command_formula": true,
"component_feedback_formula": true,
"hal_and_kins_entries_distinct": true,
"home_j0_before_transform": true,
"home_j1_before_transform": true,
"loadrt_component": true,
"servo_registration": true
},
"home_semantics": {
"home_inputs": [
"j0-motor-pos-cmd",
"j1-motor-pos-cmd"
],
"kins_difference": "corexykins homes motor joints directly",
"reason": "corexy_by_hal.hal routes Cartesian trivkins joint commands to j0home/j1home before alpha/beta stepgens",
"status": "verified"
},
"implementation_status": "verified",
"native_wasm": {
"byte_identical_json": true,
"native": {
"command": {
"alpha": 4.0,
"beta": 2.0,
"j0": 6.0,
"j1": 2.0
},
"feedback": {
"alpha": 4.0,
"beta": 1.0,
"j0": 5.0,
"j1": 3.0
},
"function_calls": 1,
"schema": "hal-corexy-sample.v1",
"tick": 1
},
"status": "passed",
"wasm": {
"command": {
"alpha": 4.0,
"beta": 2.0,
"j0": 6.0,
"j1": 2.0
},
"feedback": {
"alpha": 4.0,
"beta": 1.0,
"j0": 5.0,
"j1": 3.0
},
"function_calls": 1,
"schema": "hal-corexy-sample.v1",
"tick": 1
}
},
"oracle": {
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "765c8318c48d7570773d22394c625c0d6c479fdf2e98c62fa48296b61e99c238",
"schema": "corexy-hal-report.v1",
"source_records": {
"component": {
"path": "linuxcnc/src/hal/components/corexy_by_hal.comp",
"sha256": "7cb585058976198779600ee1d27f6b4e225a89c1c6ebcc7ba699291c9463e5a8"
},
"hal": {
"path": "linuxcnc/configs/sim/axis/corexy/corexy_by_hal.hal",
"sha256": "1199dbb80147dc3c5820833c322e9f0d9ba94ed866b1ef6d00425ad90e3156a1"
},
"hal_header": {
"path": "wasm-core/include/sim_hal.h",
"sha256": "f0b25fda1c267ed8078469c9966fa3b09721eba77c575affea77d489437a3e49"
},
"hal_source": {
"path": "wasm-core/src/sim_hal.c",
"sha256": "76a057064546a8ee9ba1098952755a4cdc56e6e714f2761b8a1826898d3ae621"
},
"hal_test": {
"path": "wasm-core/tests/sim_hal_test.c",
"sha256": "5044ed422b37499291ff85fd7c601acc6665621cf33e9ff4d26f0e8fd5b7d533"
},
"ini": {
"path": "linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini",
"sha256": "e838da30ce5fba2d049487d48bd2aeff46b1390a5a448f31980d9ce06121740f"
},
"kins_ini": {
"path": "linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini",
"sha256": "b6682cc5186944a44c2aa78501c4f350e56b4da1151348fa9fc3ece5f0eeea3a"
}
},
"status": "partial",
"transform": {
"command": {
"alpha": "0.5*(j0+j1)",
"beta": "0.5*(j0-j1)"
},
"direction": "verified against LinuxCNC corexy_by_hal.comp",
"feedback": {
"j0": "alpha+beta",
"j1": "alpha-beta"
},
"sample": {
"alpha": 4.0,
"alpha_fb": 4.0,
"beta": 2.0,
"beta_fb": 1.0,
"j0": 6.0,
"j0_fb": 5.0,
"j1": 2.0,
"j1_fb": 3.0
}
}
}

View File

@@ -0,0 +1,436 @@
{
"closure": {
"blocking_reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"cartesian_candidate_ids": [
"config:linuxcnc/configs/sim/axis/anglejog/anglejog.ini:54:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/axis.ini:144:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/axis_9axis.ini:67:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/axis_mm.ini:136:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/canterp.ini:47:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/classicladder/demo_sim_cl.ini:52:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini:10:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/db_demo/base.inc:38:kinematics:trivkins"
],
"coupled_linear_source_ids": [
"source:linuxcnc/src/emc/kinematics/corexykins.c",
"source:linuxcnc/src/emc/kinematics/rosekins.c"
],
"max_rotate_source_ids": [
"linuxcnc/src/emc/kinematics/maxkins.c",
"linuxcnc/src/emc/kinematics/rotatekins.c"
],
"official_oracle": "blocked"
},
"configuration_candidate_origin": {
"rotatekins": "workspace_derived_fixture; LinuxCNC locked source has no shipped rotatekins config"
},
"configuration_candidates": {
"corexy_hal": {
"path": "linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini",
"sha256": "e838da30ce5fba2d049487d48bd2aeff46b1390a5a448f31980d9ce06121740f"
},
"corexy_kins": {
"path": "linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini",
"sha256": "b6682cc5186944a44c2aa78501c4f350e56b4da1151348fa9fc3ece5f0eeea3a"
},
"maxkins": {
"path": "linuxcnc/configs/sim/axis/vismach/5axis/max5/max5kins.ini",
"sha256": "ea7d453127122b7831e74de2204b446c514f3c999961a4be412b36880bacb8b6"
},
"rose": {
"path": "linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini",
"sha256": "0685e9ca8c05c903b715e9fed6ecd23cbac13911ed335b6d14e24eb235de25d3"
},
"rotatekins": {
"path": "linuxcnc/configs/sim/axis/rotate/rotatekins.ini",
"sha256": "324af9a73d21b5f0c9adaf3c97e698422790d403586901f9e161b6020b1fb4fa"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"corexy_entry_distinction": {
"distinct": true,
"hal_component": "linuxcnc/src/hal/components/corexy_by_hal.comp",
"hal_configuration": "linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini",
"kins_configuration": "linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini"
},
"families": {
"corexy": {
"boundary": "configuration coordinate requirements verified",
"forward": "X=0.5*(J0+J1), Y=0.5*(J0-J1), Z=J2",
"round_trip": "verified"
},
"maxkins": {
"boundary": "pivot_length=0 and pivot_length=0.666 verified",
"forward": "LinuxCNC maxkins pivot/B/C/U/V/W correction formula",
"round_trip": "verified"
},
"rose": {
"branch_state": "previous quadrant plus revolution count",
"forward": "X=R*cos(theta), Y=R*sin(theta), Z=J1; theta=J2 degrees",
"quadrant_continuity": "verified",
"round_trip": "verified"
},
"rotatekins": {
"boundary": "90 degree rotation verified",
"forward": "XY rotation by -C degrees; Z and optional axes direct",
"round_trip": "verified"
}
},
"implementation_status": "verified",
"native_wasm": {
"native_command": "build-native/sim_kinematics_test",
"status": "passed",
"wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"
},
"report_sha256": "3bcd97e6a87058f42f1653ae896a95b6dad2ca9f149236b75a2d5d699f2e6988",
"samples": {
"byte_identical_json": true,
"native": {
"corexy": {
"inverse": [
6.0,
2.0,
3.0
],
"pose": [
4.0,
2.0,
3.0
],
"state_hash": 15406663564927631605
},
"fiveaxiskins": {
"inverse": [
10.0,
20.0,
30.0,
35.0,
40.0,
-2.0,
20.0
],
"pose": [
118.967490359,
111.434580971,
76.850293016
],
"state_hash": 5053227094931014129
},
"maxkins": {
"inverse": [
3.0,
4.0,
5.0,
0.0,
30.0,
90.0
],
"pose": [
-4.0,
3.0,
5.0
],
"state_hash": 2549915653649577904
},
"puma": {
"inverse": [
10.0,
20.0,
-30.0,
40.0,
25.0,
-15.0
],
"pose": [
450.112320486,
207.114833215,
-612.180622409
],
"state_hash": 3161234248163340028
},
"rose": {
"branch": 1,
"inverse": [
2.0,
7.0,
190.0
],
"pose": [
-1.969615506,
-0.347296355,
7.0
],
"state_hash": 15304911204640563299
},
"rotatekins": {
"inverse": [
3.0,
4.0,
5.0,
0.0,
0.0,
90.0
],
"pose": [
4.0,
-3.0,
5.0
],
"state_hash": 1338350581716392236
},
"scara": {
"inverse": [
-3.644300703,
40.0,
25.0,
-31.355699297,
5.0,
-3.0
],
"pose": [
590.460310444,
130.945742721,
465.0
],
"state_hash": 4515826253922604237
},
"schema": "kinematics-coupled-samples.v1",
"scorbot": {
"inverse": [
25.0,
35.0,
-20.0,
10.0,
-5.0
],
"pose": [
366.786994761,
171.035584569,
191.173940759
],
"state_hash": 16037922903473574131
},
"xyzac_trt": {
"inverse": [
100.0,
50.0,
200.0,
30.0,
45.0
],
"pose": [
142.826706376,
9.689621386,
163.102540378
],
"state_hash": 4869492769119734977
},
"xyzbc_trt": {
"inverse": [
100.0,
50.0,
200.0,
30.0,
45.0
],
"pose": [
44.740352549,
25.97032557,
227.67175976
],
"state_hash": 1734235358276017152
}
},
"wasm": {
"corexy": {
"inverse": [
6.0,
2.0,
3.0
],
"pose": [
4.0,
2.0,
3.0
],
"state_hash": 15406663564927631605
},
"fiveaxiskins": {
"inverse": [
10.0,
20.0,
30.0,
35.0,
40.0,
-2.0,
20.0
],
"pose": [
118.967490359,
111.434580971,
76.850293016
],
"state_hash": 5053227094931014129
},
"maxkins": {
"inverse": [
3.0,
4.0,
5.0,
0.0,
30.0,
90.0
],
"pose": [
-4.0,
3.0,
5.0
],
"state_hash": 2549915653649577904
},
"puma": {
"inverse": [
10.0,
20.0,
-30.0,
40.0,
25.0,
-15.0
],
"pose": [
450.112320486,
207.114833215,
-612.180622409
],
"state_hash": 3161234248163340028
},
"rose": {
"branch": 1,
"inverse": [
2.0,
7.0,
190.0
],
"pose": [
-1.969615506,
-0.347296355,
7.0
],
"state_hash": 15304911204640563299
},
"rotatekins": {
"inverse": [
3.0,
4.0,
5.0,
0.0,
0.0,
90.0
],
"pose": [
4.0,
-3.0,
5.0
],
"state_hash": 1338350581716392236
},
"scara": {
"inverse": [
-3.644300703,
40.0,
25.0,
-31.355699297,
5.0,
-3.0
],
"pose": [
590.460310444,
130.945742721,
465.0
],
"state_hash": 4515826253922604237
},
"schema": "kinematics-coupled-samples.v1",
"scorbot": {
"inverse": [
25.0,
35.0,
-20.0,
10.0,
-5.0
],
"pose": [
366.786994761,
171.035584569,
191.173940759
],
"state_hash": 16037922903473574131
},
"xyzac_trt": {
"inverse": [
100.0,
50.0,
200.0,
30.0,
45.0
],
"pose": [
142.826706376,
9.689621386,
163.102540378
],
"state_hash": 4869492769119734977
},
"xyzbc_trt": {
"inverse": [
100.0,
50.0,
200.0,
30.0,
45.0
],
"pose": [
44.740352549,
25.97032557,
227.67175976
],
"state_hash": 1734235358276017152
}
}
},
"schema": "coupled-kinematics-report.v1",
"source_records": {
"corexy": {
"path": "linuxcnc/src/emc/kinematics/corexykins.c",
"sha256": "507963e0ed464440fa530db11b1b15c6a663182eb016855d105554ae8125ae5b"
},
"maxkins": {
"path": "linuxcnc/src/emc/kinematics/maxkins.c",
"sha256": "01529e0db1b698af621d63c30bc8ae382757dd367f3e4babc9b959873344b4e7"
},
"rose": {
"path": "linuxcnc/src/emc/kinematics/rosekins.c",
"sha256": "1de39affa6b99f21c5e902fc67c8b1d8876d44fc8eff920afd816d7f956cfd78"
},
"rotatekins": {
"path": "linuxcnc/src/emc/kinematics/rotatekins.c",
"sha256": "b104c7e0eb726f48b83daa4d596db5ca8021d803ab17dd123db9b9410ce5f47c"
}
},
"state_hash": {
"native": {
"corexy": 15406663564927631605,
"maxkins": 2549915653649577904,
"rose": 15304911204640563299,
"rotatekins": 1338350581716392236
},
"stable": true,
"wasm": {
"corexy": 15406663564927631605,
"maxkins": 2549915653649577904,
"rose": 15304911204640563299,
"rotatekins": 1338350581716392236
}
},
"status": "partial"
}

View File

@@ -0,0 +1,361 @@
{
"configuration_candidate_count": 4,
"configuration_candidates": {
"linear_axis": {
"hal_files": [
"sim_ldelta.hal",
"axis_manualtoolchange.hal"
],
"kinematics": "lineardeltakins",
"missing_hal_files": [
"linuxcnc/configs/sim/axis/axis_manualtoolchange.hal",
"linuxcnc/configs/sim/axis/sim_ldelta.hal"
],
"path": "linuxcnc/configs/sim/axis/ldelta.ini",
"sha256": "1567138c3d54b4d9712b5031f6a0b060edadd8d1442d1eff8d93c2beefbb5e0c"
},
"linear_demo": {
"hal_files": [
"sim_ldelta.hal",
"axis_manualtoolchange.hal"
],
"kinematics": "lineardeltakins",
"missing_hal_files": [
"linuxcnc/configs/sim/axis/axis_manualtoolchange.hal",
"linuxcnc/configs/sim/axis/sim_ldelta.hal"
],
"path": "linuxcnc/configs/sim/axis/ldelta_demo.ini",
"sha256": "ab0645308abe903bf66f36a328b43303aa0e8ad2d19e69a45e2703d665956439"
},
"linear_tklinuxcnc": {
"hal_files": [
"sim_ldelta.hal",
"axis_manualtoolchange.hal"
],
"kinematics": "lineardeltakins",
"missing_hal_files": [
"linuxcnc/configs/sim/tklinuxcnc/axis_manualtoolchange.hal",
"linuxcnc/configs/sim/tklinuxcnc/sim_ldelta.hal"
],
"path": "linuxcnc/configs/sim/tklinuxcnc/ldelta.ini",
"sha256": "0e23dee0f4df839c028f5bfba95c5941f2f8623ce502d6fdfc90dfdef63d8967"
},
"rotary_axis": {
"hal_files": [
"sim_rdelta.hal",
"axis_manualtoolchange.hal"
],
"kinematics": "rotarydeltakins",
"missing_hal_files": [
"linuxcnc/configs/sim/axis/axis_manualtoolchange.hal",
"linuxcnc/configs/sim/axis/sim_rdelta.hal"
],
"path": "linuxcnc/configs/sim/axis/rdelta.ini",
"sha256": "53510733b8fd0230547f48f76ebd35dc5c98f58fe1dba1246777e227ef82c508"
}
},
"families": {
"linear_delta": {
"configuration_candidates": [
"linear_axis",
"linear_demo",
"linear_tklinuxcnc"
],
"forward": "three carriage spheres -> quadratic platform root",
"inverse": "three carriage heights from sphere intersections",
"module": "lineardeltakins"
},
"rotary_delta": {
"configuration_candidates": [
"rotary_axis"
],
"forward": "three rotary knee constraints -> quadratic platform root",
"inverse": "three rotated leg intersections",
"module": "rotarydeltakins"
}
},
"geometry": {
"linear": {
"radius": 130.25,
"rod_length": 269.0,
"tower_layout": "120-degree equilateral",
"units": "mm"
},
"rotary": {
"foot_radius": 6.0,
"joint_units": "degrees",
"platform_radius": 10.0,
"shin_length": 14.0,
"thigh_length": 10.0
}
},
"implementation_status": "verified",
"mapping": {
"coordinates": "XYZ",
"extra_joint_policy": "prefix only; extra joints rejected by count",
"kinematics_joint_count": 3,
"optional_axes": "A/B/C/U/V/W pass through"
},
"native_wasm": {
"native_command": "build-native/sim_kinematics_test",
"status": "passed",
"wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"
},
"oracle": {
"blocked_samples": [
"linear nominal/root",
"linear alternate root",
"linear no-solution",
"linear joint-limit",
"rotary nominal/root",
"rotary alternate root",
"rotary z=0 singular"
],
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "a585edd0895eefb4add8753927a33b60cae806e814623830df39f396787cd2e3",
"resource_gaps": {
"missing_hal_files": [
"linuxcnc/configs/sim/axis/axis_manualtoolchange.hal",
"linuxcnc/configs/sim/axis/sim_ldelta.hal",
"linuxcnc/configs/sim/axis/sim_rdelta.hal",
"linuxcnc/configs/sim/tklinuxcnc/axis_manualtoolchange.hal",
"linuxcnc/configs/sim/tklinuxcnc/sim_ldelta.hal"
],
"policy": "retained as missing inputs; no configuration smoke is promoted to numeric golden"
},
"samples": {
"byte_identical_json": true,
"native": {
"delta_edges": {
"lineardelta": {
"alternate_branch": 1,
"alternate_pose": [
-0.0,
-0.0,
535.363415806
],
"alternate_status": 0,
"branch": 0,
"geometry_error": -703,
"iterations": 0,
"limit_error": -702,
"no_solution": -702
},
"rotarydelta": {
"alternate_branch": 1,
"alternate_pose": [
-0.0,
-0.0,
0.320396615
],
"alternate_status": 0,
"branch": 0,
"geometry_error": -703,
"iterations": 0,
"singular": -704
},
"tripod": {
"alternate_branch": 1,
"alternate_pose": [
2.0,
3.0,
-4.0
],
"alternate_status": 0,
"branch": 0,
"geometry_error": -703,
"iterations": 0,
"limit_error": -702,
"no_solution": -702,
"singular": -704
}
},
"lineardelta": {
"inverse": [
300.0,
300.0,
300.0
],
"pose": [
0.0,
0.0,
64.636584194
],
"state_hash": 10022623683197000937
},
"rotarydelta": {
"inverse": [
10.0,
10.0,
10.0
],
"pose": [
0.0,
0.0,
-3.793360169
],
"state_hash": 14346691006498560820
}
},
"state_hash_identical": true,
"wasm": {
"delta_edges": {
"lineardelta": {
"alternate_branch": 1,
"alternate_pose": [
-0.0,
-0.0,
535.363415806
],
"alternate_status": 0,
"branch": 0,
"geometry_error": -703,
"iterations": 0,
"limit_error": -702,
"no_solution": -702
},
"rotarydelta": {
"alternate_branch": 1,
"alternate_pose": [
-0.0,
-0.0,
0.320396615
],
"alternate_status": 0,
"branch": 0,
"geometry_error": -703,
"iterations": 0,
"singular": -704
},
"tripod": {
"alternate_branch": 1,
"alternate_pose": [
2.0,
3.0,
-4.0
],
"alternate_status": 0,
"branch": 0,
"geometry_error": -703,
"iterations": 0,
"limit_error": -702,
"no_solution": -702,
"singular": -704
}
},
"lineardelta": {
"inverse": [
300.0,
300.0,
300.0
],
"pose": [
0.0,
0.0,
64.636584194
],
"state_hash": 10022623683197000937
},
"rotarydelta": {
"inverse": [
10.0,
10.0,
10.0
],
"pose": [
0.0,
0.0,
-3.793360169
],
"state_hash": 14346691006498560820
}
}
},
"schema": "delta-kinematics-report.v1",
"solver": {
"alternate_root_flag": "SIM_KIN_DELTA_ALTERNATE_ROOT bit 0",
"convergence_tolerance": 0.0,
"default_root": 0,
"fixed_iteration_limit": 0,
"forward": "analytic quadratic intersection",
"inverse": "three independent leg intersections"
},
"source_records": {
"header": {
"path": "wasm-core/include/sim_kinematics.h",
"sha256": "43e588f7c45c703970598e8e42ff5aae4b97a437eaff1d4cd6ad156bd204edc7"
},
"implementation": {
"path": "wasm-core/src/sim_kinematics.c",
"sha256": "f15842a1dbf0cb33f071ee1e70715a85a1f3dec44c4facc220edd924f6ca352c"
},
"linear_c": {
"path": "linuxcnc/src/emc/kinematics/lineardeltakins.c",
"sha256": "792caf5f3fb8bf06d97a4bb7262697ac22182d78d91fed19e7a9e537585aa748"
},
"linear_cc": {
"path": "linuxcnc/src/emc/kinematics/lineardeltakins.cc",
"sha256": "77634457a4dd3f5617f91a66651965c5b8326fc0d4dfc916d9cd63b89b0ec0f1"
},
"linear_common": {
"path": "linuxcnc/src/emc/kinematics/lineardeltakins-common.h",
"sha256": "7785bcd7d095b431169cdd0f753c177ece643ee825f7ff4b8cdb6f8cd4706df8"
},
"rotary_c": {
"path": "linuxcnc/src/emc/kinematics/rotarydeltakins.c",
"sha256": "64c8aeb6998a481f3c26743093dd157d2d22dc6769b258e3e6012b5c1f784773"
},
"rotary_cc": {
"path": "linuxcnc/src/emc/kinematics/rotarydeltakins.cc",
"sha256": "7e2fd63a2a7f5718d706faa21f903b36007226b065ad6d234631d2514f653618"
},
"rotary_common": {
"path": "linuxcnc/src/emc/kinematics/rotarydeltakins-common.h",
"sha256": "ba3e037fe2ad7eba29aa2b657492ac83f1979a38c9249fef52e598ba0d5c9136"
},
"test": {
"path": "wasm-core/tests/sim_kinematics_test.c",
"sha256": "00d9efc3c701ba2848535357f6027e6d9d4d19796a17be96e290ddc0babe2c5c"
}
},
"status": "partial",
"workspace_and_boundaries": {
"linear": {
"alternate_branch": 1,
"alternate_root_pose": [
-0.0,
-0.0,
535.363415806
],
"geometry_error": -703,
"iterations": 0,
"joint_limit_error": -702,
"no_solution_error": -702,
"nominal": [
0.0,
0.0,
64.636584194
],
"root_branch": 0
},
"rotary": {
"alternate_branch": 1,
"alternate_root_pose": [
-0.0,
-0.0,
0.320396615
],
"geometry_error": -703,
"iterations": 0,
"nominal": [
0.0,
0.0,
-3.793360169
],
"root_branch": 0,
"singular_error": -704
}
}
}

View File

@@ -0,0 +1,105 @@
{
"boundary": {
"duplicate_axis_suffix": "verified",
"missing_required_coordinate": "SIM_KIN_ERR_CONFIGURATION verified",
"pivot_length": "250.0 verified",
"round_trip": "verified",
"state_hash": "native/WASM identical"
},
"configuration": {
"coordinates": "xyzbcwy",
"duplicate_axis": "Y at joint 1 and suffix joint 6; first-wins mapping with suffix synchronized",
"joints": 7,
"kinematics": "5axiskins",
"path": "linuxcnc/configs/sim/axis/vismach/5axis/bridgemill/5axis.ini",
"pivot_length_default": 250.0,
"required_coordinates": "XYZBCW",
"scan": {
"joint_count": true,
"kins_entry": true,
"pivot_pin": true
}
},
"formula": {
"forward": "X=JX+r*sin(180-B)*cos(C), Y=JY+r*sin(180-B)*sin(C), Z=JZ+pivot+r*cos(180-B)",
"inverse": "subtract the same spherical vector, then map pose to coordinates",
"source_contains_required_coordinates": true,
"source_spherical_helper": "s2r"
},
"implementation_status": "verified",
"native_wasm": {
"native_command": "build-native/sim_kinematics_test",
"status": "passed",
"wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"
},
"oracle": {
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "d0af8c3785d87bedd869d235f45b9d522434589a26794113b1acf8526982dde0",
"sample": {
"byte_identical_json": true,
"native": {
"inverse": [
10.0,
20.0,
30.0,
35.0,
40.0,
-2.0,
20.0
],
"pose": [
118.967490359,
111.434580971,
76.850293016
],
"state_hash": 5053227094931014129
},
"wasm": {
"inverse": [
10.0,
20.0,
30.0,
35.0,
40.0,
-2.0,
20.0
],
"pose": [
118.967490359,
111.434580971,
76.850293016
],
"state_hash": 5053227094931014129
}
},
"schema": "fiveaxis-kinematics-report.v1",
"source_records": {
"config": {
"path": "linuxcnc/configs/sim/axis/vismach/5axis/bridgemill/5axis.ini",
"sha256": "789ba656f8ee3e97feba07b58eb242305898e745795891b1a7e536289bffe6bf"
},
"hal": {
"path": "linuxcnc/configs/sim/axis/vismach/5axis/bridgemill/5axisgui.hal",
"sha256": "a59e6b9432c0a898f6d8da46309b868733a906158733fb0942017d14eb97cd07"
},
"header": {
"path": "wasm-core/include/sim_kinematics.h",
"sha256": "db16293ec3645559a0be71a40963693f3d1e3f5fdedc92902be19b47f653dfa8"
},
"source": {
"path": "linuxcnc/src/emc/kinematics/5axiskins.c",
"sha256": "2e29fee21936cfbbafbb000290276e4b32eaba4831014e08d3fb833f2b3dac72"
},
"source_impl": {
"path": "wasm-core/src/sim_kinematics.c",
"sha256": "7a77915c59f190bcbe5c93d8f53498745bfcf978c167ba8878a21b914f9401fd"
},
"test": {
"path": "wasm-core/tests/sim_kinematics_test.c",
"sha256": "ceeb8b4100d034d76e70ce675d78b2d62c51e8e70be2205a9c8ae02e105a6aa6"
}
},
"status": "partial"
}

View File

@@ -0,0 +1,333 @@
{
"configuration_candidate": {
"coordinates": "XYZABC",
"hal_geometry": {
"base": [
-22.95,
13.25,
0.0,
22.95,
13.25,
0.0,
22.95,
13.25,
0.0,
0.0,
-26.5,
0.0,
0.0,
-26.5,
0.0,
-22.95,
13.25,
0.0
],
"platform": [
-1.0,
11.5,
0.0,
1.0,
11.5,
0.0,
10.459,
-4.884,
0.0,
9.459,
-6.616,
0.0,
-9.459,
-6.616,
0.0,
-10.459,
-4.884,
0.0
]
},
"hal_pins": [
"last-iterations",
"max-iterations",
"limit-iterations",
"max-error",
"convergence-criterion",
"fwd-kins-fail",
"x",
"y",
"z",
"a",
"b",
"c"
],
"joints": 6,
"kinematics": "genhexkins",
"path": "linuxcnc/configs/sim/axis/vismach/hexapod-sim/hexapod.ini",
"scan": true
},
"geometry": {
"base_points": [
[
-22.95,
13.25,
0.0
],
[
22.95,
13.25,
0.0
],
[
22.95,
13.25,
0.0
],
[
0.0,
-26.5,
0.0
],
[
0.0,
-26.5,
0.0
],
[
-22.95,
13.25,
0.0
]
],
"platform_points": [
[
-1.0,
11.5,
0.0
],
[
1.0,
11.5,
0.0
],
[
10.459,
-4.884,
0.0
],
[
9.459,
-6.616,
0.0
],
[
-9.459,
-6.616,
0.0
],
[
-10.459,
-4.884,
0.0
]
],
"struts": 6,
"units": "configuration units; XYZ linear and ABC degrees"
},
"implementation_status": "verified",
"mapping": {
"coordinates": "XYZABC",
"extra_joint_policy": "rejected",
"joint_semantics": [
"strut-0",
"strut-1",
"strut-2",
"strut-3",
"strut-4",
"strut-5"
],
"kinematics_joint_count": 6
},
"native_wasm": {
"native_command": "build-native/sim_kinematics_test",
"status": "passed",
"wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"
},
"oracle": {
"blocked_samples": [
"nominal six-leg pose",
"alternate initial seed",
"nonpositive strut",
"iteration limit",
"maximum error",
"singular Jacobian",
"joint limit"
],
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "992b96bae770789e6013cfffaeb3c47d8ecfde24d7abe974987e66250921ea78",
"samples": {
"byte_identical_json": true,
"native": {
"boundaries": {
"alternate_branch": 1,
"alternate_pose": [
10.0,
-5.0,
-60.0
],
"alternate_status": 0,
"branch": 0,
"iteration_limit": -702,
"iterations": 14,
"limit_error": -702,
"max_error": -702,
"nonpositive": -702,
"residual": 8.57e-10,
"singular": -704
},
"genhex": {
"inverse": [
68.274402001,
63.00792855,
63.783820692,
65.475127133,
60.456723205,
68.231500501
],
"iterations": 14,
"pose": [
10.0,
-5.0,
60.0,
4.999999999,
-3.000000001,
10.0
],
"residual": 8.57e-10,
"state_hash": 11890997469032613683
}
},
"state_hash_identical": true,
"wasm": {
"boundaries": {
"alternate_branch": 1,
"alternate_pose": [
10.0,
-5.0,
-60.0
],
"alternate_status": 0,
"branch": 0,
"iteration_limit": -702,
"iterations": 14,
"limit_error": -702,
"max_error": -702,
"nonpositive": -702,
"residual": 8.57e-10,
"singular": -704
},
"genhex": {
"inverse": [
68.274402001,
63.00792855,
63.783820692,
65.475127133,
60.456723205,
68.231500501
],
"iterations": 14,
"pose": [
10.0,
-5.0,
60.0,
4.999999999,
-3.000000001,
10.0
],
"residual": 8.57e-10,
"state_hash": 11890997469032613683
}
}
},
"schema": "genhex-kinematics-report.v1",
"solver": {
"alternate_seed_flag": "SIM_KIN_GENHEX_ALTERNATE_SEED bit 1",
"default_convergence_tolerance": 1e-09,
"default_iteration_limit": 120,
"default_max_error": 500.0,
"forward": "Newton-Raphson 6x6 Jacobian",
"inverse": "closed-form six strut lengths",
"seed": [
0.0,
0.0,
50.0,
0.0,
0.0,
0.0
]
},
"source_records": {
"config": {
"path": "linuxcnc/configs/sim/axis/vismach/hexapod-sim/hexapod.ini",
"sha256": "362654b77bcaf5c87c3aa14c12684f516cbd635b6d582a68b05fcbd8299822ea"
},
"hal": {
"path": "linuxcnc/configs/sim/axis/vismach/hexapod-sim/kinematics.hal",
"sha256": "6dd6d63e0403dcc0c82b43790eaa5b4a4d639c5a02391ac1dcd5673969c4ec39"
},
"header": {
"path": "linuxcnc/src/emc/kinematics/genhexkins.h",
"sha256": "580a2e1703a5c0efb683b4a26f805af434edcfe9d1ade7890e44307f8b0b236c"
},
"implementation": {
"path": "wasm-core/src/sim_kinematics.c",
"sha256": "6abae835c16feb27ec1dc28a9ea5384f0bc5190217ba04a4997048d83066b905"
},
"kinematics_header": {
"path": "wasm-core/include/sim_kinematics.h",
"sha256": "939d4088d9e7120e4a405be154eb3d98cd4e5925fc27d47641c115624c1bd994"
},
"source": {
"path": "linuxcnc/src/emc/kinematics/genhexkins.c",
"sha256": "8df3d8bd812e040a06bc29ea91625fd143bacf26903b106ae6700430aca7f6b9"
},
"test": {
"path": "wasm-core/tests/sim_kinematics_test.c",
"sha256": "c9e4d20973c6a487787504bd5de9a73829f53d8bbfd35f91e72c6c62c76f191e"
}
},
"status": "partial",
"workspace_and_boundaries": {
"alternate_seed": [
10.0,
-5.0,
-60.0
],
"iteration_limit_error": -702,
"iterations": 14,
"joint_limit_error": -702,
"max_error_boundary": -702,
"nominal": {
"inverse": [
68.274402001,
63.00792855,
63.783820692,
65.475127133,
60.456723205,
68.231500501
],
"iterations": 14,
"pose": [
10.0,
-5.0,
60.0,
4.999999999,
-3.000000001,
10.0
],
"residual": 8.57e-10,
"state_hash": 11890997469032613683
},
"nonpositive_joint_error": -702,
"residual": 8.57e-10,
"singular_jacobian_error": -704
}
}

View File

@@ -0,0 +1,132 @@
{
"boundaries": {
"fixed_iteration_limit": "verified",
"invalid_DH_nonfinite": "SIM_KIN_ERR_NONFINITE verified",
"state_hash": "native/WASM identical after 1e-9 quantization"
},
"configuration_candidates": {
"melfa": {
"dh_source": "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa_dh.hal",
"kinematics": "genserkins",
"path": "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini",
"scan": true
},
"puma560": {
"dh_source": "linuxcnc/configs/sim/axis/vismach/puma/puma560_dh.hal",
"kinematics": "genserkins",
"path": "linuxcnc/configs/sim/axis/vismach/puma/puma560.ini",
"scan": true
}
},
"dh_contract": {
"alpha_units": "radians from HAL pins",
"joint_count": 6,
"joint_units": "degrees at public ABI, radians internally",
"melfa_has_alpha_and_d": true,
"parameters": [
"A-0..5",
"ALPHA-0..5",
"D-0..5"
],
"puma_has_alpha_and_d": true
},
"implementation_status": "verified",
"native_wasm": {
"native_command": "build-native/sim_kinematics_test",
"status": "passed",
"wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"
},
"oracle": {
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "fc6c23f67b2a14b58735fa60ed884a5a73815c3efb26f1b4d0de427e882d67b8",
"samples": {
"byte_identical_json": true,
"native": {
"inverse": [
0.0,
10.0,
-20.0,
29.999999999,
15.0,
-9.999999999
],
"iterations": 0,
"pose": [
619.051467462,
30.4112378,
-349.855537721
],
"residual": 0.0,
"state_hash": 13985782206955965200
},
"wasm": {
"inverse": [
0.0,
10.0,
-20.0,
29.999999999,
15.0,
-9.999999999
],
"iterations": 0,
"pose": [
619.051467462,
30.4112378,
-349.855537721
],
"residual": 0.0,
"state_hash": 13985782206955965200
}
},
"schema": "genser-kinematics-report.v1",
"solver": {
"convergence_tolerance": 1e-07,
"forward": "Craig DH homogeneous product",
"inverse": "fixed-zero seed damped numerical Jacobian with 1e-4 degree finite difference",
"iterations": 0,
"max_iterations": 200,
"nonconvergence": "SIM_KIN_ERR_SINGULAR after fixed iteration limit",
"residual": 0.0
},
"source_records": {
"functions": {
"path": "linuxcnc/src/emc/kinematics/genserfuncs.c",
"sha256": "e4b775e93b973a4faae873c81a95d7d8361fe5c5c59ea81e362a5f900a1e8781"
},
"header": {
"path": "wasm-core/include/sim_kinematics.h",
"sha256": "5fd01043d705486b0d365e36109c3b0e0d39f220c2cec313b5b747dca06148bc"
},
"implementation": {
"path": "wasm-core/src/sim_kinematics.c",
"sha256": "41fc146ab88a623b5bc6afbba1fc0d12434689c737426da12fcd297162672749"
},
"melfa_config": {
"path": "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini",
"sha256": "7dc7a3db58ca7f7ddba11fe08f691cd7658564be2a0b872d9087ee7b392e45c7"
},
"melfa_dh": {
"path": "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa_dh.hal",
"sha256": "0d943182e37da99ae99db53f3dd464cb9b62b17d4cabe56c35c0c27b43bf68c6"
},
"puma_config": {
"path": "linuxcnc/configs/sim/axis/vismach/puma/puma560.ini",
"sha256": "2bb881ed6c5662282977accac29bfd6c4efc18e572cbcb8a31c134677012cf4c"
},
"puma_dh": {
"path": "linuxcnc/configs/sim/axis/vismach/puma/puma560_dh.hal",
"sha256": "23758eb7a6a4abe116e1de88140d04e6c9a910d1c378f019d76843a5a3fb6141"
},
"source": {
"path": "linuxcnc/src/emc/kinematics/genserkins.c",
"sha256": "cbb87b9c21f9f5e1d6a3c5bd333f72a7b1b4f1618a21b32d4b34bc621b43e075"
},
"test": {
"path": "wasm-core/tests/sim_kinematics_test.c",
"sha256": "6c803de9aeb91499cef391620f17c7da69ee78a873b43b0ba9fa508fd096f31a"
}
},
"status": "partial"
}

View File

@@ -5,5 +5,8 @@
"fixed_tick_scheduler": true, "fixed_tick_scheduler": true,
"max_period_ticks": 1000000, "max_period_ticks": 1000000,
"native_tests": ["sim_hal_test"], "native_tests": ["sim_hal_test"],
"wasm_tests": ["sim_hal_wasm_test"],
"static_components": ["corexy_by_hal"],
"corexy_transform": "coverage/corexy-hal.report.json",
"real_excluded": ["hardware_driver", "linux_realtime_kernel", "host_device_access"] "real_excluded": ["hardware_driver", "linux_realtime_kernel", "host_device_access"]
} }

View File

@@ -0,0 +1,361 @@
{
"canonical": {
"block_hash": "fnv1a64 over input bytes",
"byte_identical": true,
"deterministic_runs": 2,
"format": "fixed-order JSON with IEEE-754 binary64 %.17g values",
"state_hash": "fnv1a64 over explicit state fields",
"state_hash_identical": true
},
"error_boundaries": [
"argument",
"nonfinite",
"syntax",
"range",
"unknown_code",
"state",
"buffer"
],
"golden_tolerance": "exact_for_supported_words",
"implementation": "linuxcnc/src/emc/rs274ngc",
"inputs": {
"bytes": 1133481,
"file_count": 55,
"files": [
{
"bytes": 2058,
"path": "linuxcnc/src/emc/rs274ngc/Submakefile",
"sha256": "9a684add8b229eab90fe047ff3b5c7c18cfdbe96c7a2926a9c94518d12bcfbbc"
},
{
"bytes": 5174,
"path": "linuxcnc/src/emc/rs274ngc/array1.hh",
"sha256": "352f170d36ba34c57efb4a7b5739de29740234821b47b02fc6bdf9261b9e45b9"
},
{
"bytes": 619,
"path": "linuxcnc/src/emc/rs274ngc/boost_pyenum_macros.hh",
"sha256": "ae537267442894365ba483c66da60d86e28d39f31a493fd698650d9e7765b14e"
},
{
"bytes": 13230,
"path": "linuxcnc/src/emc/rs274ngc/canonmodule.cc",
"sha256": "7b47e3f67c8f4a4370b2957cc3bf6e17ae0820a2853f4febc9ec9598627c6acd"
},
{
"bytes": 44952,
"path": "linuxcnc/src/emc/rs274ngc/gcodemodule.cc",
"sha256": "de4fed54839271a850898123958acafddab17995a102597814a90452e24441a9"
},
{
"bytes": 17113,
"path": "linuxcnc/src/emc/rs274ngc/interp_arc.cc",
"sha256": "86a31b1110506a992c846c0333a5443da88ed82800e94df879688d6493664c42"
},
{
"bytes": 13874,
"path": "linuxcnc/src/emc/rs274ngc/interp_array.cc",
"sha256": "590f39ac0344aa2d3f4344565f5a73328ae305432a6c0f2d48557ef298bb6715"
},
{
"bytes": 2181,
"path": "linuxcnc/src/emc/rs274ngc/interp_array_types.hh",
"sha256": "1898b92511dbe3052fd765100b9ef95b65c62903cba9188b3f918f8725b16155"
},
{
"bytes": 2270,
"path": "linuxcnc/src/emc/rs274ngc/interp_base.cc",
"sha256": "1b6dc7c836bcb2bcfdb66975320458894ed6a1a1a90d9d4f6b4af938408e0366"
},
{
"bytes": 2945,
"path": "linuxcnc/src/emc/rs274ngc/interp_base.hh",
"sha256": "cf57996a4549a8d001b346892e4fa88de0008e83681e429040a21635aa06337b"
},
{
"bytes": 16098,
"path": "linuxcnc/src/emc/rs274ngc/interp_check.cc",
"sha256": "241b4778d5ab0939d695ad07e0b4b177750731e17e29d2406e8781ad7adfc2e2"
},
{
"bytes": 243966,
"path": "linuxcnc/src/emc/rs274ngc/interp_convert.cc",
"sha256": "50cff00c7760e51ebdd1afc8bafb863fe3f0559976313a21fccdc0a002a321f6"
},
{
"bytes": 89996,
"path": "linuxcnc/src/emc/rs274ngc/interp_cycles.cc",
"sha256": "eaced8d8f33fb7840b6b345fcb1ad9ea3ba46003599ba4ee98aebc1e98d8ccc2"
},
{
"bytes": 13102,
"path": "linuxcnc/src/emc/rs274ngc/interp_execute.cc",
"sha256": "a7787d0ae340184f1fe5055ec52dee054a0e67626db6ecfe55418e3e4c99b53c"
},
{
"bytes": 27920,
"path": "linuxcnc/src/emc/rs274ngc/interp_find.cc",
"sha256": "35ec59cca1711f5ae1b99e7effe5cde18fcad998e3180324ee87a5a0815ffcfb"
},
{
"bytes": 1016,
"path": "linuxcnc/src/emc/rs274ngc/interp_fwd.hh",
"sha256": "2fe42f0fed1247b400dd946cd976d2ca83707d33e05cdbe223ebcca6a862e9e9"
},
{
"bytes": 33574,
"path": "linuxcnc/src/emc/rs274ngc/interp_g7x.cc",
"sha256": "27648c21410e3a85d26b684ad2bd6604fcd6317a429bf799744fb718fa0760da"
},
{
"bytes": 1382,
"path": "linuxcnc/src/emc/rs274ngc/interp_inspection.cc",
"sha256": "1d3417b4e420f89b7d09dd719e976b68ff39860df8ba374724c1a821cf0df354"
},
{
"bytes": 1224,
"path": "linuxcnc/src/emc/rs274ngc/interp_inspection.hh",
"sha256": "d6add770b4cf94c8cb05578b349ce9928c57c55476e8f2b551f10e0ba018e81e"
},
{
"bytes": 15598,
"path": "linuxcnc/src/emc/rs274ngc/interp_internal.cc",
"sha256": "6276d990a7ebaec6a5a793f5d0f49364a32e69db4f770d11627b9ea6dc45e809"
},
{
"bytes": 33425,
"path": "linuxcnc/src/emc/rs274ngc/interp_internal.hh",
"sha256": "619832fceccf05aba18e84a4bf9b15b47f2ce80ef1cb5f1a3fe737fcd1d05781"
},
{
"bytes": 5015,
"path": "linuxcnc/src/emc/rs274ngc/interp_inverse.cc",
"sha256": "c3d02097143a11b2d27b84c05da906250e8b8f42c3ce6b6b3eb9c7f7514344d7"
},
{
"bytes": 28616,
"path": "linuxcnc/src/emc/rs274ngc/interp_namedparams.cc",
"sha256": "35e1a40ec555052d6895fa53ba4fc013c835456771f01522218dc006e67396ca"
},
{
"bytes": 39144,
"path": "linuxcnc/src/emc/rs274ngc/interp_o_word.cc",
"sha256": "32f3722cb7bb90eb7db4cf5c82d3781c6880a1e7810107c2365edec7e9520d8c"
},
{
"bytes": 5304,
"path": "linuxcnc/src/emc/rs274ngc/interp_parameter_def.hh",
"sha256": "95979af5cd58188d01fd02c33ac3b85472c8fd4a31041e531e785d9d6d2594da"
},
{
"bytes": 10063,
"path": "linuxcnc/src/emc/rs274ngc/interp_python.cc",
"sha256": "4872d5d58902e370f856c1b516f2f0c6d4693544f4493d62dd247422aeb2a26e"
},
{
"bytes": 609,
"path": "linuxcnc/src/emc/rs274ngc/interp_python.hh",
"sha256": "d8aa266335990d1c3a29549a00c99f195526ceee6190d869c17c64b11596fdda"
},
{
"bytes": 25017,
"path": "linuxcnc/src/emc/rs274ngc/interp_queue.cc",
"sha256": "71bb7350c6933fa7b6f6c964e5cb4fa88e4f56444142f87381e611a77eec77ba"
},
{
"bytes": 4513,
"path": "linuxcnc/src/emc/rs274ngc/interp_queue.hh",
"sha256": "c78f0e9ddb5fcbccf168c92d58fbf2a8600d62dee515af7f68347bcb0dafa64d"
},
{
"bytes": 123494,
"path": "linuxcnc/src/emc/rs274ngc/interp_read.cc",
"sha256": "72473355a0a1cef08bf922ee4d8b89f46210397957f9b1336254223eae66e116"
},
{
"bytes": 17533,
"path": "linuxcnc/src/emc/rs274ngc/interp_remap.cc",
"sha256": "e0891ff3133f7e2a3fc6ff76542f102bc8f46887e27fc8aa8117208fc8cd066b"
},
{
"bytes": 5616,
"path": "linuxcnc/src/emc/rs274ngc/interp_setup.cc",
"sha256": "f80e7793e21d62db33cd45cb0c7b9d242a74aac07dc8171a1102734e9c40e23f"
},
{
"bytes": 11421,
"path": "linuxcnc/src/emc/rs274ngc/interp_write.cc",
"sha256": "03e6a28345f714af9656355c5c6cbb6e132db431f0c6ca9568d5be8c3081eea0"
},
{
"bytes": 39664,
"path": "linuxcnc/src/emc/rs274ngc/interpmodule.cc",
"sha256": "96e11690c472e679525c911b350db603b7869f41a3dd79b984b99fd134561fb1"
},
{
"bytes": 684,
"path": "linuxcnc/src/emc/rs274ngc/meson.build",
"sha256": "98c1f76f3e1f9d4a0a0d78362a7aa85eb15e528bf1b79a0d7b4e7306a933a4d9"
},
{
"bytes": 1891,
"path": "linuxcnc/src/emc/rs274ngc/modal_state.cc",
"sha256": "2d28d76c8ca636594f4e83d25ae53365bfe733bd3b3af5673343f41e9a7130c8"
},
{
"bytes": 1962,
"path": "linuxcnc/src/emc/rs274ngc/modal_state.hh",
"sha256": "14459b9fab3d5818682f948b1411cff27bd36a7dbbf2cc9144ee1f365e5fbe0e"
},
{
"bytes": 33231,
"path": "linuxcnc/src/emc/rs274ngc/nurbs_additional_functions.cc",
"sha256": "64225f521e4067420cfa773dc93061fea198e3c8a9fc5937b62a6bcc76e80e30"
},
{
"bytes": 1436,
"path": "linuxcnc/src/emc/rs274ngc/paramclass.hh",
"sha256": "24d3d283359a9dc063dfeaf37b8a3105079df7ea4d89adb7553cabcba708d34d"
},
{
"bytes": 2544,
"path": "linuxcnc/src/emc/rs274ngc/pyarrays.cc",
"sha256": "00f6fc2eadb0426f2fb54443b097d13a79c1899e8cdcc98b6d873f35f9688a55"
},
{
"bytes": 5418,
"path": "linuxcnc/src/emc/rs274ngc/pyblock.cc",
"sha256": "e1861749e7eb4e2d6e9b5c212f48f5ec452be3a76af1fe66ea6b47b410aafbd2"
},
{
"bytes": 4067,
"path": "linuxcnc/src/emc/rs274ngc/pyemctypes.cc",
"sha256": "855ec693fb86f75b3d420808ce29691d01d09d9a664315def5cf79ed2b24d4af"
},
{
"bytes": 4550,
"path": "linuxcnc/src/emc/rs274ngc/pyinterp1.cc",
"sha256": "c1db7bae54730c987a466c81f23307ec33a5cd65fd07623fa5f473f2f455e4c2"
},
{
"bytes": 4338,
"path": "linuxcnc/src/emc/rs274ngc/pyparamclass.cc",
"sha256": "e685ca3ba693bd8f329bcc4dedd7b36d05db2556fc51a09e6549f4d9dbeebb88"
},
{
"bytes": 2392,
"path": "linuxcnc/src/emc/rs274ngc/rs274ngc.hh",
"sha256": "00c303490e8c44133603eeb1d661b16678468818b8e25f75adda981b3a757e57"
},
{
"bytes": 31741,
"path": "linuxcnc/src/emc/rs274ngc/rs274ngc_interp.hh",
"sha256": "c8bbcaa7a9545aa80055459dd3217a0b91bbc3ec034791752b5b25dea0c46fe0"
},
{
"bytes": 94199,
"path": "linuxcnc/src/emc/rs274ngc/rs274ngc_pre.cc",
"sha256": "73935d127557c0e8f9da6267f629351ec5198fbce05c665c678819383901d61b"
},
{
"bytes": 16799,
"path": "linuxcnc/src/emc/rs274ngc/rs274ngc_return.hh",
"sha256": "183974f6b7a15bf7e1578cf07d2a2ede1b188427d52368105ba7e9d28af96b7c"
},
{
"bytes": 1582,
"path": "linuxcnc/src/emc/rs274ngc/units.h",
"sha256": "a2d9148bab60dbb6f2ea41bd7b13ccd44986568b805711e0a12a6aff23c39a08"
},
{
"bytes": 1282,
"path": "linuxcnc/unit_tests/interp/interp_testing_util.hh",
"sha256": "18cb2d6075d163fff58e2892f84455f3cc3491e653e76c6531601eac39362d56"
},
{
"bytes": 179,
"path": "linuxcnc/unit_tests/interp/meson.build",
"sha256": "534b25743c0259e3757cc2b60fbe0edad23473a5262bc988992d576fb364385e"
},
{
"bytes": 23093,
"path": "linuxcnc/unit_tests/interp/test_interp_basics.cc",
"sha256": "0929d736e50c854624102d70d673d0eb15ce93a05af7993429b6f19deff57e74"
},
{
"bytes": 3135,
"path": "linuxcnc/unit_tests/interp/test_interp_block.cc",
"sha256": "c2c063b7255bb3b1a15ce2be34d5998a877235b18eca5435df487c8c87dfb81d"
},
{
"bytes": 637,
"path": "linuxcnc/unit_tests/interp/test_string_conversion.cc",
"sha256": "d6b056f762826da85385d0e235de4dc4d8f96a2b8ef64abe140c7baed09f6754"
},
{
"bytes": 595,
"path": "linuxcnc/unit_tests/interp/tests_main.cc",
"sha256": "f7ac2692b082bd9309535d2bebe11ace840395b00b63b64180b2857501956a4f"
}
],
"fingerprint_sha256": "ddf2667de41902ecdabb6c2466fe43236dafe4d17fa2c3b8ca37c398920eac02",
"roots": [
"linuxcnc/src/emc/rs274ngc",
"linuxcnc/unit_tests/interp"
]
},
"native_and_wasm": true,
"outputs": {
"bytes": 15523,
"file_count": 3,
"files": [
{
"bytes": 1872,
"path": "wasm-core/include/sim_interpreter.h",
"sha256": "7bf24098340e3da2272061acf7a43376c6c40fe9dffc55d012e3af65ceffedbb"
},
{
"bytes": 11585,
"path": "wasm-core/src/sim_interpreter.c",
"sha256": "9f9f45f7c123be774dcf55a752a29e6cc1f873a1605cf89c4caa4f4ab125e6a4"
},
{
"bytes": 2066,
"path": "wasm-core/tests/sim_interpreter_test.c",
"sha256": "c4fc3ba11b751ed9f3f770ff1ff164ce18aa19986003cf85d17a26de7aecd3ce"
}
],
"fingerprint_sha256": "440a472b1f24c6bc092e0181f58389a2fd7fab2fdb632583f1c650ec8db86d6e"
},
"schema": "interpreter-runtime-report.v1",
"status": "passed",
"supported_words": [
"G0",
"G1",
"G2",
"G3",
"G17",
"G18",
"G19",
"G20",
"G21",
"G90",
"G91",
"M2",
"M3",
"M4",
"M5",
"M30",
"X",
"Y",
"Z",
"A",
"B",
"C",
"U",
"V",
"W",
"F",
"S"
]
}

View File

@@ -0,0 +1,231 @@
{
"abi_version": 1,
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"contract_sha256": "3c338760a86122d25b76e5a27e26b9c79d8579e1ca96638ed932b8ca8e41283a",
"declared_functions": [
"sim_kinematics_forward",
"sim_kinematics_forward_ex",
"sim_kinematics_home",
"sim_kinematics_init",
"sim_kinematics_inverse",
"sim_kinematics_inverse_ex",
"sim_kinematics_state_hash",
"sim_kinematics_validate_pose"
],
"enums": {
"sim_kinematics_mode": [
{
"name": "SIM_KIN_MODE_DEFAULT",
"value": 0
},
{
"name": "SIM_KIN_MODE_IDENTITY",
"value": 1
},
{
"name": "SIM_KIN_MODE_FORWARD_ONLY",
"value": 2
},
{
"name": "SIM_KIN_MODE_INVERSE_ONLY",
"value": 3
},
{
"name": "SIM_KIN_MODE_BOTH",
"value": 4
}
],
"sim_kinematics_result": [
{
"name": "SIM_KIN_OK",
"value": 0
},
{
"name": "SIM_KIN_ERR_ARGUMENT",
"value": -700
},
{
"name": "SIM_KIN_ERR_NONFINITE",
"value": -701
},
{
"name": "SIM_KIN_ERR_RANGE",
"value": -702
},
{
"name": "SIM_KIN_ERR_CONFIGURATION",
"value": -703
},
{
"name": "SIM_KIN_ERR_SINGULAR",
"value": -704
},
{
"name": "SIM_KIN_ERR_UNKNOWN_TYPE",
"value": -705
},
{
"name": "SIM_KIN_ERR_UNSUPPORTED",
"value": -706
}
],
"sim_kinematics_type": [
{
"name": "SIM_KIN_TRIVKINS",
"value": 1
},
{
"name": "SIM_KIN_XYZAC_TRT",
"value": 2
},
{
"name": "SIM_KIN_XYZBC_TRT",
"value": 3
},
{
"name": "SIM_KIN_XYZBCW",
"value": 4
},
{
"name": "SIM_KIN_COREXY",
"value": 5
},
{
"name": "SIM_KIN_ROSE",
"value": 6
},
{
"name": "SIM_KIN_MAXKINS",
"value": 7
},
{
"name": "SIM_KIN_ROTATEKINS",
"value": 8
},
{
"name": "SIM_KIN_5AXISKINS",
"value": 9
},
{
"name": "SIM_KIN_SCARA",
"value": 10
},
{
"name": "SIM_KIN_PUMA",
"value": 11
},
{
"name": "SIM_KIN_SCORBOT",
"value": 12
},
{
"name": "SIM_KIN_GENSER",
"value": 13
}
]
},
"errors": {
"SIM_KIN_ERR_ARGUMENT": -700,
"SIM_KIN_ERR_CONFIGURATION": -703,
"SIM_KIN_ERR_NONFINITE": -701,
"SIM_KIN_ERR_RANGE": -702,
"SIM_KIN_ERR_SINGULAR": -704,
"SIM_KIN_ERR_UNKNOWN_TYPE": -705,
"SIM_KIN_ERR_UNSUPPORTED": -706,
"SIM_KIN_OK": 0
},
"exported_functions": [
"sim_kinematics_forward",
"sim_kinematics_forward_ex",
"sim_kinematics_home",
"sim_kinematics_init",
"sim_kinematics_inverse",
"sim_kinematics_inverse_ex",
"sim_kinematics_state_hash",
"sim_kinematics_validate_pose"
],
"inputs": {
"cmake": "wasm-core/CMakeLists.txt",
"cmake_sha256": "e9296a3983a531e4e6864d05e2e97c8dbf29e0dc9e45942ecc1a0a7222b3a660",
"header": "wasm-core/include/sim_kinematics.h",
"header_sha256": "5fd01043d705486b0d365e36109c3b0e0d39f220c2cec313b5b747dca06148bc"
},
"missing_required_exports": [],
"numeric_policy": {
"fast_math": false,
"fp_contract": "off",
"iteration_limit": 100000,
"nonfinite": "SIM_KIN_ERR_NONFINITE",
"state_hash": "FNV-1a-64"
},
"required_functions": [
"sim_kinematics_init",
"sim_kinematics_forward",
"sim_kinematics_forward_ex",
"sim_kinematics_inverse",
"sim_kinematics_inverse_ex",
"sim_kinematics_home",
"sim_kinematics_validate_pose",
"sim_kinematics_state_hash"
],
"schema": "kinematics-abi-contract.v1",
"status": "verified",
"structs": {
"sim_kinematics": [
"sim_kinematics_config config;",
"int axis_joint[SIM_KIN_MAX_JOINTS];",
"uint32_t configured;",
"uint64_t forward_flags;",
"uint64_t inverse_flags;",
"sim_posemath_pose last_pose;",
"double last_residual;",
"uint32_t last_iterations;",
"uint32_t branch_id;",
"int32_t revolution_count;",
"uint32_t previous_quadrant;"
],
"sim_kinematics_config": [
"int type;",
"int mode;",
"uint32_t joint_count;",
"uint32_t kinematics_joint_count;",
"uint32_t extra_joint_count;",
"char coordinates[16];",
"double x_rot_point;",
"double y_rot_point;",
"double z_rot_point;",
"double x_offset;",
"double y_offset;",
"double z_offset;",
"double tool_offset;",
"double pivot_length;",
"double scara_d1;",
"double scara_d2;",
"double scara_d3;",
"double scara_d4;",
"double scara_d5;",
"double scara_d6;",
"double puma_a2;",
"double puma_a3;",
"double puma_d3;",
"double puma_d4;",
"double puma_d6;",
"uint32_t dh_joint_count;",
"double dh_a[SIM_KIN_MAX_JOINTS];",
"double dh_alpha[SIM_KIN_MAX_JOINTS];",
"double dh_d[SIM_KIN_MAX_JOINTS];",
"uint32_t max_iterations;",
"double convergence_tolerance;",
"uint8_t limit_enabled[SIM_KIN_MAX_JOINTS];",
"double joint_min[SIM_KIN_MAX_JOINTS];",
"double joint_max[SIM_KIN_MAX_JOINTS];"
],
"sim_kinematics_result_info": [
"uint64_t forward_flags;",
"uint64_t inverse_flags;",
"double residual;",
"uint32_t iterations;",
"uint32_t branch_id;"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
{
"$id": "linuxcnc-web-wasm/kinematics-golden.v1",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"context_id": {
"type": "string"
},
"input": {
"type": "object"
},
"operation": {
"const": "fk_ik_fk_roundtrip"
},
"result": {
"additionalProperties": true,
"required": [
"status",
"error_code",
"kinematics_type",
"branch_id",
"flags",
"current_tick",
"config_hash",
"axis_joint_mapping"
],
"type": "object"
},
"sample_id": {
"type": "string"
},
"schema": {
"const": "kinematics-golden.v1"
},
"tolerances": {
"additionalProperties": {
"minimum": 0,
"type": "number"
},
"type": "object"
}
},
"required": [
"schema",
"context_id",
"sample_id",
"operation",
"input",
"result"
],
"title": "LinuxCNC Web/WASM kinematics FK/IK golden",
"type": "object"
}

View File

@@ -0,0 +1,92 @@
{
"axis_order": {
"A": 3,
"B": 4,
"C": 5,
"U": 6,
"V": 7,
"W": 8,
"X": 0,
"Y": 1,
"Z": 2
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"contract_sha256": "f79ad4af7f460b0d544d835fa63fe331d7c39907d7394edba1c7a3cbef612874",
"error_cases": [
"null_config",
"null_pose",
"nonfinite_joint",
"nonfinite_pose",
"joint_count_mismatch",
"enabled_limit_violation",
"invalid_coordinate",
"inverse_only_forward",
"forward_only_inverse"
],
"inputs": {
"header": "/home/mes006/linuxcncWebWASM/wasm-core/include/sim_kinematics.h",
"header_sha256": "db16293ec3645559a0be71a40963693f3d1e3f5fdedc92902be19b47f653dfa8",
"source": "/home/mes006/linuxcncWebWASM/wasm-core/src/sim_kinematics.c",
"source_sha256": "7a77915c59f190bcbe5c93d8f53498745bfcf978c167ba8878a21b914f9401fd"
},
"sample_mappings": [
{
"axis_joint": {
"X": 0,
"Y": 1,
"Z": 2
},
"coordinates": "XYZ"
},
{
"axis_joint": {
"X": 0,
"Y": 1,
"Z": 3
},
"coordinates": "XYYZ",
"duplicate": {
"axis": "Y",
"copies": [
2
],
"owner": 1
}
},
{
"axis_joint": {
"X": 0,
"Y": 1,
"Z": 3
},
"case_fold": true,
"coordinates": "xyyz"
},
{
"axis_joint": {
"A": 3,
"B": 4,
"C": 5,
"U": 6,
"V": 7,
"W": 8,
"X": 0,
"Y": 1,
"Z": 2
},
"coordinates": "XYZABCUVW"
}
],
"schema": "kinematics-mapping-contract.v1",
"semantics": {
"coordinate_case": "ASCII case-fold to uppercase",
"duplicate_axis": "first occurrence owns axis; later duplicates copy the first joint during inverse",
"extra_joints": "coordinates may repeat and joint_count remains authoritative",
"forward": "joint-to-pose uses mapped first joint for each axis",
"home": "zeroes all configured joints, then computes forward pose",
"inverse": "pose-to-joint initializes all joints then fills duplicates deterministically",
"limits": "enabled inclusive min/max; violation returns SIM_KIN_ERR_RANGE",
"state_hash": "includes config bytes, configured joint prefix and pose bytes"
},
"status": "verified"
}

View File

@@ -0,0 +1,74 @@
{
"angles": {
"constants": "full-precision DEG_TO_RAD/RAD_TO_DEG literals",
"input_units": "degrees for rotary joints",
"internal_units": "radians"
},
"boundary_matrix": [
{
"case": "NaN joint",
"expected": "SIM_KIN_ERR_NONFINITE"
},
{
"case": "+Inf pose",
"expected": "SIM_KIN_ERR_NONFINITE"
},
{
"case": "negative tolerance",
"expected": "SIM_KIN_ERR_CONFIGURATION"
},
{
"case": "iteration > 100000",
"expected": "SIM_KIN_ERR_CONFIGURATION"
},
{
"case": "XYZBC B=90 singular",
"expected": "SIM_KIN_ERR_SINGULAR"
},
{
"case": "enabled limit violation",
"expected": "SIM_KIN_ERR_RANGE"
}
],
"compiler": {
"denormals": "host-independent finite checks before arithmetic",
"fast_math": false,
"fenv_access": "default",
"fp_contract": "off"
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"exact_fields": [
"status",
"error_code",
"kinematics_type",
"branch_id",
"flags",
"current_tick",
"config_hash",
"axis_joint_mapping",
"event_order"
],
"inputs": {
"abi_contract_sha256": "3177c4d28200c5136981d28489ac4578a84360f9688ee8614e23af8d7526b50b",
"mapping_contract_sha256": "49bd93c716bd5b5eb6064f75ea2b8ba26dfa819c6166c22c5375ebeba927dd91"
},
"iterations": {
"default": 0,
"maximum": 100000,
"nonconvergence": "stable error; never repaired by tolerance widening",
"zero_means": "algorithm default"
},
"policy_sha256": "dcf2fdb6bf488f81ec55f43ebc3180f52665b54ba9cd9b4c6bbfb8dbd2a39560",
"schema": "kinematics-numeric-policy.v1",
"status": "verified",
"tolerances": {
"configuration_nonnegative": true,
"golden_absolute_default": 1e-09,
"golden_relative_default": 1e-09,
"singularity_threshold": 1e-12,
"state_hash_float_quantization": 1e-09,
"tolerance_applies_only_to": [
"finite numeric pose/joint fields"
]
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,599 @@
{
"blocked_count": 11,
"blocking_reasons": [
"linuxcnc/unit_tests/kinematics"
],
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"golden_schema": "coverage/kinematics-golden-schema.json",
"golden_schema_sha256": "8d6abe21ad350bb5a5ddc38d8fd9044ed2fa622f4331804a85cad55eb5a30a63",
"inputs": {
"closure": "coverage/kinematics-source-closure.report.json",
"closure_sha256": "d3af5e19e38d8dc2640323dcf755d0323e871815087b9eece9d76544b0b0cfa6",
"samples": "coverage/kinematics-oracle-samples.jsonl",
"samples_sha256": "df32ade49e3c3beb49e52d26200e37766dd04c5bee5dba4b1c87695439bf87bd"
},
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"ready_count": 0,
"records": [
{
"configuration_id": "config:linuxcnc/configs/sim/axis/anglejog/anglejog.ini:54:kinematics:trivkins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "cartesian",
"input": {
"joint": [
10.0,
-5.0,
2.5
]
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "cartesian",
"status": "blocked"
},
"sample_id": "kinematics-sample:cartesian:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/configs/sim/axis/corexy/corexy_by_hal.hal:25:hal_transform:corexy_by_hal",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "coupled_linear",
"input": {
"joint": [
12.0,
4.0,
3.0
]
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "coupled_linear",
"status": "blocked"
},
"sample_id": "kinematics-sample:coupled_linear:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/configs/sim/axis/vismach/5axis/bridgemill/5axis.ini:98:kinematics:5axiskins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "five_axis",
"input": {
"joint": [
10.0,
20.0,
30.0,
15.0,
-20.0
]
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "five_axis",
"status": "blocked"
},
"sample_id": "kinematics-sample:five_axis:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini:31:kinematics:xyzab_tdr_kins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "five_axis_trt",
"input": {
"joint": [
100.0,
50.0,
200.0,
30.0,
45.0
]
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "five_axis_trt",
"status": "blocked"
},
"sample_id": "kinematics-sample:five_axis_trt:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/configs/sim/axis/ldelta.ini:59:kinematics:lineardeltakins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "parallel_delta",
"input": {
"pose": {
"abcuvw": [
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"tran": [
5.0,
-3.0,
-250.0
]
}
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "parallel_delta",
"status": "blocked"
},
"sample_id": "kinematics-sample:parallel_delta:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/configs/sim/axis/vismach/hexapod-sim/hexapod.ini:76:kinematics:genhexkins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "parallel_hexapod",
"input": {
"pose": {
"abcuvw": [
1.0,
-1.0,
2.0,
0.0,
0.0,
0.0
],
"tran": [
0.0,
0.0,
100.0
]
}
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "parallel_hexapod",
"status": "blocked"
},
"sample_id": "kinematics-sample:parallel_hexapod:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/configs/sim/axis/pentapod/pentapod.ini:113:kinematics:pentakins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "parallel_pentapod",
"input": {
"pose": {
"abcuvw": [
1.0,
2.0,
0.0,
0.0,
0.0,
0.0
],
"tran": [
0.0,
0.0,
75.0
]
}
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "parallel_pentapod",
"status": "blocked"
},
"sample_id": "kinematics-sample:parallel_pentapod:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/configs/sim/tklinuxcnc/tripod.ini:59:kinematics:tripodkins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "parallel_tripod",
"input": {
"pose": {
"abcuvw": [
2.0,
-1.0,
0.0,
0.0,
0.0,
0.0
],
"tran": [
1.0,
2.0,
100.0
]
}
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "parallel_tripod",
"status": "blocked"
},
"sample_id": "kinematics-sample:parallel_tripod:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini:7:kinematics:genserkins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "serial_robot",
"input": {
"joint": [
0.0,
-30.0,
45.0,
10.0,
20.0,
-15.0
]
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "serial_robot",
"status": "blocked"
},
"sample_id": "kinematics-sample:serial_robot:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini:3:switchkins:switchkins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "switchable",
"input": {
"joint": [
5.0,
6.0,
7.0,
30.0
],
"switch_sequence": [
0,
1,
0
]
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "switchable",
"status": "blocked"
},
"sample_id": "kinematics-sample:switchable:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
},
{
"configuration_id": "config:linuxcnc/tests/matrixkins/test.ini:53:kinematics:matrixkins",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"execution": {
"native_smoke": {
"binary": "build-native/sim_kinematics_test",
"exit_code": 0,
"result_sha256": "96d4ce027200030704ead75b891ead3c3af9b39f60eaccaafa875e0e8d526e3a",
"status": "passed",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_sha256": "4792c9d03a753904bfef11d4b53e680ec1ff00a8b593addd95f4d566aa722140"
},
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle",
"status": "blocked"
},
"family": "user_static",
"input": {
"pose": {
"abcuvw": [
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"tran": [
10.0,
20.0,
30.0
]
}
},
"operation": "fk_ik_fk_roundtrip",
"result": {
"axis_joint_mapping": null,
"branch_id": null,
"config_hash": null,
"current_tick": 0,
"error_code": "MISSING_OFFICIAL_ORACLE",
"flags": null,
"kinematics_type": "user_static",
"status": "blocked"
},
"sample_id": "kinematics-sample:user_static:nominal-roundtrip",
"schema": "kinematics-golden.v1",
"tolerances": {
"absolute_tolerance": 1e-09,
"exact_fields": [
"status",
"flags",
"branch_id"
],
"invariant": "pose_roundtrip"
}
}
],
"report_fingerprint_sha256": "a501a60ae74340f05642468dc4911f56b9c5a3e93b0ccdb2e2a11657754cac7e",
"sample_count": 11,
"schema": "kinematics-oracle-report.v1",
"status": "blocked"
}

View File

@@ -0,0 +1,286 @@
{
"closure": {
"blocking_reason": "linuxcnc/unit_tests/kinematics is absent; official oracle remains blocked while the deterministic runtime slice is implemented",
"full_linuxcnc_kinematics": false,
"official_unit_tests_present": false,
"oracle_ready": false
},
"determinism": {
"byte_identical": true,
"native_ctest": "passed",
"runs": 2,
"state_hash_identical": true
},
"error_boundaries": [
"argument",
"nonfinite",
"range",
"configuration",
"singular",
"unsupported",
"unknown_type"
],
"implementation": "wasm-core/src/sim_kinematics.c",
"inputs": {
"bytes": 241271,
"file_count": 38,
"files": [
{
"bytes": 9705,
"path": "linuxcnc/src/emc/kinematics/5axiskins.c",
"sha256": "2e29fee21936cfbbafbb000290276e4b32eaba4831014e08d3fb833f2b3dac72"
},
{
"bytes": 1620,
"path": "linuxcnc/src/emc/kinematics/Submakefile",
"sha256": "1c207052f7321b3380891762da32b8985592c3dba3cc894a8fd946229809db69"
},
{
"bytes": 1202,
"path": "linuxcnc/src/emc/kinematics/blend.fig",
"sha256": "8ccf10df11d69958f286b3c8012fc239a07093d3789e5be9c96a6d487b92a89b"
},
{
"bytes": 2272,
"path": "linuxcnc/src/emc/kinematics/corexykins.c",
"sha256": "507963e0ed464440fa530db11b1b15c6a663182eb016855d105554ae8125ae5b"
},
{
"bytes": 9497,
"path": "linuxcnc/src/emc/kinematics/cubic.c",
"sha256": "64f9070d413fa0ebf94668d753e13d17049317899777b59ea100b30db00acda3"
},
{
"bytes": 1764,
"path": "linuxcnc/src/emc/kinematics/cubic.h",
"sha256": "00a9d34bb44045890221eeb0f44bd93b76b695b6e788405a8b6f78deae338436"
},
{
"bytes": 26308,
"path": "linuxcnc/src/emc/kinematics/genhexkins.c",
"sha256": "8df3d8bd812e040a06bc29ea91625fd143bacf26903b106ae6700430aca7f6b9"
},
{
"bytes": 3680,
"path": "linuxcnc/src/emc/kinematics/genhexkins.h",
"sha256": "580a2e1703a5c0efb683b4a26f805af434edcfe9d1ade7890e44307f8b0b236c"
},
{
"bytes": 22068,
"path": "linuxcnc/src/emc/kinematics/genserfuncs.c",
"sha256": "e4b775e93b973a4faae873c81a95d7d8361fe5c5c59ea81e362a5f900a1e8781"
},
{
"bytes": 2313,
"path": "linuxcnc/src/emc/kinematics/genserkins.c",
"sha256": "cbb87b9c21f9f5e1d6a3c5bd333f72a7b1b4f1618a21b32d4b34bc621b43e075"
},
{
"bytes": 5247,
"path": "linuxcnc/src/emc/kinematics/genserkins.h",
"sha256": "2e40cf061dafe050ad242f197b4acdbd916adcea41885e4edbb2b8c7b5423833"
},
{
"bytes": 9696,
"path": "linuxcnc/src/emc/kinematics/kinematics.h",
"sha256": "33b3744b9ceeeef3d992e9ca9cda89d93a3357d2d0a2af2023ca728e4be8e428"
},
{
"bytes": 12821,
"path": "linuxcnc/src/emc/kinematics/kins_util.c",
"sha256": "9a9dfbeb0f5d307b5fe99556b10a1869ccbcabc699be1bcfcfd80864ffb0288e"
},
{
"bytes": 4416,
"path": "linuxcnc/src/emc/kinematics/lineardeltakins-common.h",
"sha256": "7785bcd7d095b431169cdd0f753c177ece643ee825f7ff4b8cdb6f8cd4706df8"
},
{
"bytes": 2489,
"path": "linuxcnc/src/emc/kinematics/lineardeltakins.c",
"sha256": "792caf5f3fb8bf06d97a4bb7262697ac22182d78d91fed19e7a9e537585aa748"
},
{
"bytes": 1808,
"path": "linuxcnc/src/emc/kinematics/lineardeltakins.cc",
"sha256": "77634457a4dd3f5617f91a66651965c5b8326fc0d4dfc916d9cd63b89b0ec0f1"
},
{
"bytes": 3763,
"path": "linuxcnc/src/emc/kinematics/maxkins.c",
"sha256": "01529e0db1b698af621d63c30bc8ae382757dd367f3e4babc9b959873344b4e7"
},
{
"bytes": 699,
"path": "linuxcnc/src/emc/kinematics/meson.build",
"sha256": "a5de13a16e378a98c1c3df262881592396818510e835a5f2fe2101ed512072a3"
},
{
"bytes": 14441,
"path": "linuxcnc/src/emc/kinematics/pentakins.c",
"sha256": "1b385c2ab3a1bcb106d1d22a2dfe14519dba9c4a5df4cb31071a180845cda4c6"
},
{
"bytes": 1560,
"path": "linuxcnc/src/emc/kinematics/pentakins.h",
"sha256": "af8551454177bbcc85e4e17377e059573ab8c15dc022de2a4be9b70dead74e2d"
},
{
"bytes": 10870,
"path": "linuxcnc/src/emc/kinematics/pumakins.c",
"sha256": "5d473d34beacd696fc34719658e1622a1228494fe6596e277f4c34a51dfb91b2"
},
{
"bytes": 1232,
"path": "linuxcnc/src/emc/kinematics/pumakins.h",
"sha256": "a90cafe680533e0a8d43cf72c9f6bea380267d60090ea82ead4e3eb58bcddf58"
},
{
"bytes": 3839,
"path": "linuxcnc/src/emc/kinematics/rosekins.c",
"sha256": "1de39affa6b99f21c5e902fc67c8b1d8876d44fc8eff920afd816d7f956cfd78"
},
{
"bytes": 5753,
"path": "linuxcnc/src/emc/kinematics/rotarydeltakins-common.h",
"sha256": "ba3e037fe2ad7eba29aa2b657492ac83f1979a38c9249fef52e598ba0d5c9136"
},
{
"bytes": 2964,
"path": "linuxcnc/src/emc/kinematics/rotarydeltakins.c",
"sha256": "64c8aeb6998a481f3c26743093dd157d2d22dc6769b258e3e6012b5c1f784773"
},
{
"bytes": 1835,
"path": "linuxcnc/src/emc/kinematics/rotarydeltakins.cc",
"sha256": "7e2fd63a2a7f5718d706faa21f903b36007226b065ad6d234631d2514f653618"
},
{
"bytes": 2372,
"path": "linuxcnc/src/emc/kinematics/rotatekins.c",
"sha256": "b104c7e0eb726f48b83daa4d596db5ca8021d803ab17dd123db9b9410ce5f47c"
},
{
"bytes": 7467,
"path": "linuxcnc/src/emc/kinematics/scarakins.c",
"sha256": "f6e37f7cf5b932fc27378ab9ed03ef71c2010aa38f883980a1b6c6b9265cf9f9"
},
{
"bytes": 11893,
"path": "linuxcnc/src/emc/kinematics/scorbot-kins.c",
"sha256": "89badd55ec168139143c58f67c64dbdb9e5b411c3b847404beaf79ba8c591b5d"
},
{
"bytes": 11718,
"path": "linuxcnc/src/emc/kinematics/switchkins.c",
"sha256": "4de32a4a147e5ceee5346b9c6bf471dddf5c64ff58afe5044406c50bcd96b7f5"
},
{
"bytes": 1184,
"path": "linuxcnc/src/emc/kinematics/switchkins.h",
"sha256": "f6e5323aa16daaac8e07bba622d0bf969102ad0d9c00d41621c08af31e0bb9b1"
},
{
"bytes": 9211,
"path": "linuxcnc/src/emc/kinematics/tripodkins.c",
"sha256": "0fb0876d2ab190e3bde69d3fe1ce0cebbb54adee2ddb4b05b929d7ca5cb3f31f"
},
{
"bytes": 2429,
"path": "linuxcnc/src/emc/kinematics/trivkins.c",
"sha256": "a08cd347b8dde7d7e201ddde93a1cdef47e8126e58f355d4270fccf6d48819bc"
},
{
"bytes": 13363,
"path": "linuxcnc/src/emc/kinematics/trtfuncs.c",
"sha256": "ced2f9d4732681e2e82153b2f6343eccdf6972222d558d1e277bf1ec0bdc87fd"
},
{
"bytes": 11327,
"path": "linuxcnc/src/emc/kinematics/ugenserkins.c",
"sha256": "dee63aebaa11f2867657bc4b9052f6a8e8bd875e06bd8a9df2d3edac9eb1c31d"
},
{
"bytes": 2653,
"path": "linuxcnc/src/emc/kinematics/userkfuncs.c",
"sha256": "146238645955f8d4e2cb0cd34ff44db3a9dad01e149f81c08f8a53c19f1609bc"
},
{
"bytes": 1896,
"path": "linuxcnc/src/emc/kinematics/xyzac-trt-kins.c",
"sha256": "24c633613db9876c2ec246e1519a63d288fd27c68296d89f89f0368105634e69"
},
{
"bytes": 1896,
"path": "linuxcnc/src/emc/kinematics/xyzbc-trt-kins.c",
"sha256": "d79c06db82930ef685809a87c7207398f7893f56a6a0dfe382df2c81eaeb9562"
}
],
"fingerprint_sha256": "ffde02b34018456bed7d47117425a239aa7205ccd0cb40ef021e80c42e85d9f2",
"roots": [
"linuxcnc/src/emc/kinematics",
"linuxcnc/unit_tests/kinematics"
]
},
"outputs": {
"bytes": 109761,
"file_count": 4,
"files": [
{
"bytes": 5446,
"path": "wasm-core/include/sim_kinematics.h",
"sha256": "939d4088d9e7120e4a405be154eb3d98cd4e5925fc27d47641c115624c1bd994"
},
{
"bytes": 66830,
"path": "wasm-core/src/sim_kinematics.c",
"sha256": "6abae835c16feb27ec1dc28a9ea5384f0bc5190217ba04a4997048d83066b905"
},
{
"bytes": 37392,
"path": "wasm-core/tests/sim_kinematics_test.c",
"sha256": "c9e4d20973c6a487787504bd5de9a73829f53d8bbfd35f91e72c6c62c76f191e"
},
{
"bytes": 93,
"path": "wasm-core/tests/sim_kinematics_test_main.c",
"sha256": "e567fe412145d3c9b04afc91d6efc6a53c3af3b3b52b7dda860cc289d42dbfc0"
}
],
"fingerprint_sha256": "a2c4d27905dd0b153450b195aee62967773733428df8236779ee418b6d15cc13"
},
"registered_build_targets": {
"native_ctest": true,
"native_sim_core": true,
"wasm_exports": [
"sim_kinematics_init",
"sim_kinematics_forward",
"sim_kinematics_forward_ex",
"sim_kinematics_inverse",
"sim_kinematics_inverse_ex",
"sim_kinematics_home"
],
"wasm_module": true
},
"schema": "kinematics-runtime-report.v1",
"status": "partial",
"supported_types": [
"trivkins",
"xyzac-trt",
"xyzbc-trt",
"xyzbcw",
"corexy",
"rose",
"maxkins",
"rotatekins",
"5axiskins",
"scara",
"puma",
"scorbot",
"genser",
"linear-delta",
"rotary-delta",
"tripod",
"genhex"
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,370 @@
{
"canonical": {
"byte_identical": true,
"deterministic_runs": 2,
"format": "fixed-order JSON",
"state_hash": "fnv1a64 over explicit state fields",
"state_hash_identical": true
},
"error_boundaries": [
"argument",
"nonfinite",
"range",
"geometry",
"motion_type",
"state",
"capacity",
"buffer"
],
"golden_tolerance": "exact_for_fixed_tick_inputs",
"implementation": "linuxcnc/src/emc/tp + linuxcnc/src/emc/motion + linuxcnc/src/emc/task",
"inputs": {
"bytes": 1030532,
"file_count": 60,
"files": [
{
"bytes": 11,
"path": "linuxcnc/src/emc/motion/.gitignore",
"sha256": "d3914918e9a01b5a71fe22bbbd13b428373ca2a6435b302e874f0c0dd78e05b2"
},
{
"bytes": 253,
"path": "linuxcnc/src/emc/motion/Submakefile",
"sha256": "d1c3457c41726a616fce23ff177669f71d0f718b568658903ccd313399f715d3"
},
{
"bytes": 23244,
"path": "linuxcnc/src/emc/motion/axis.c",
"sha256": "5c3fe99e672a72cc81c2ce74e36e225ff9e10a115c51f46729b135fd134922d8"
},
{
"bytes": 2027,
"path": "linuxcnc/src/emc/motion/axis.h",
"sha256": "aba94067deedbd163cfcb8ed3dbc1843b947f4ef8828360490521039d784f2df"
},
{
"bytes": 71866,
"path": "linuxcnc/src/emc/motion/command.c",
"sha256": "9d138ed5780a11ed037aa238781f4bfb4960a73ee23c0b0abf0724db068b3e3f"
},
{
"bytes": 84689,
"path": "linuxcnc/src/emc/motion/control.c",
"sha256": "fa1b14689e2e4493c02d0939f982278366d4c1888da62e172cec030904c82c50"
},
{
"bytes": 4525,
"path": "linuxcnc/src/emc/motion/dbuf.c",
"sha256": "605378315317999ee069477282134e64e9940e939892dbb5f433f77f103262ef"
},
{
"bytes": 2169,
"path": "linuxcnc/src/emc/motion/dbuf.h",
"sha256": "7f15618d30d07040f58110a9c1130140f6fb14705971578e8af554e3ef6dda87"
},
{
"bytes": 2202,
"path": "linuxcnc/src/emc/motion/emcmotcfg.h",
"sha256": "26ce8b18ae3b53e001215bc92330f65301de8832cc8764b239672c0a30e9e6f3"
},
{
"bytes": 1112,
"path": "linuxcnc/src/emc/motion/emcmotglb.c",
"sha256": "3fe47c93af71036340c517046062b6ce955fdd78c7369bc26aeff2fe51e2c4ab"
},
{
"bytes": 1314,
"path": "linuxcnc/src/emc/motion/emcmotglb.h",
"sha256": "7ba82587da304f62aa261e4647639cdcc8f7623699677462b82a19d503e7332f"
},
{
"bytes": 1925,
"path": "linuxcnc/src/emc/motion/emcmotutil.c",
"sha256": "275d601af6fdf115ecf1ca580c3d307ed5a150a8c9d03cf8cf071268e715d020"
},
{
"bytes": 1057,
"path": "linuxcnc/src/emc/motion/homemod.c",
"sha256": "df2e24e8d6c48d47448971b6e69471910a435d207a9d45f857a968aebf14ea5c"
},
{
"bytes": 59456,
"path": "linuxcnc/src/emc/motion/homing.c",
"sha256": "ee73d38f2feb8d5523b9a733cb17d635d3904667aaa219f8a02d7fbffee1ba39"
},
{
"bytes": 2265,
"path": "linuxcnc/src/emc/motion/homing.dot",
"sha256": "df3be34e18b97fdacef55e26dd8df9d1af9f1984d3782d460a3646e7e5dbe53d"
},
{
"bytes": 2868,
"path": "linuxcnc/src/emc/motion/homing.h",
"sha256": "fe33498e5a9c2a5aee6aebdab57a93742cba61018af8bd83f1d79edb28dbe0ed"
},
{
"bytes": 40,
"path": "linuxcnc/src/emc/motion/meson.build",
"sha256": "4b5493eb6bb0f9d745ede01e162b71a39c1572ab335ef493ced4c2554b6cc887"
},
{
"bytes": 15972,
"path": "linuxcnc/src/emc/motion/mot_priv.h",
"sha256": "92ea1ec7f18374e2f8867348586776a680dfec724ea373a257d77f36cc640c6b"
},
{
"bytes": 48444,
"path": "linuxcnc/src/emc/motion/motion.c",
"sha256": "c3d1404150b56cfb03ab35ecdf64694d60a6d37117d0617a8464d548133e79d3"
},
{
"bytes": 31857,
"path": "linuxcnc/src/emc/motion/motion.h",
"sha256": "640f10ec3d02d2955f1ffacd86e5b7078a631ff3c6703f201e0d04b177a17225"
},
{
"bytes": 1024,
"path": "linuxcnc/src/emc/motion/motion_struct.h",
"sha256": "2af32cecf7128793bbfcbb2840da8b7f32d836ce2b4524541581274bfd7ebe1f"
},
{
"bytes": 2315,
"path": "linuxcnc/src/emc/motion/simple_tp.c",
"sha256": "5a0026f4d5bdd9d1bc55290c6ea9c399abd664633b69413c307f7353953d2603"
},
{
"bytes": 2046,
"path": "linuxcnc/src/emc/motion/simple_tp.h",
"sha256": "a6b72ddaf4a14801f95b625d824c2ea1625db5dce765db2d3b1f96a4730afca7"
},
{
"bytes": 3561,
"path": "linuxcnc/src/emc/motion/stashf.c",
"sha256": "3017fd1732c037729c9bd1eb998752b225774f51c11bf76f159b7669d9176fe2"
},
{
"bytes": 1289,
"path": "linuxcnc/src/emc/motion/stashf.h",
"sha256": "5fcbde679a78958552430b3af95fbf4d3012669ecaf1a28c68f4ca9f16efbddd"
},
{
"bytes": 2859,
"path": "linuxcnc/src/emc/motion/stashf_wrap.h",
"sha256": "625cc7d8df30b351f243bc3f1687f5ef9758cb4243efd0c2196d9c71c64c7793"
},
{
"bytes": 4167,
"path": "linuxcnc/src/emc/motion/state_tag.h",
"sha256": "e53b2989929b43aeb90167e0b352aeaddbe55344b0e7e5ccdfb81881a7eaffd1"
},
{
"bytes": 5895,
"path": "linuxcnc/src/emc/motion/teleop-notes",
"sha256": "6ac654e450157beb0ded79386f4b59ac08553f452be544f27b091f4b97030ae4"
},
{
"bytes": 19321,
"path": "linuxcnc/src/emc/motion/usrmotintf.cc",
"sha256": "44e3413daef771bf6104eb48ae7279ff60625b87c19063bf3097639fb8e1d818"
},
{
"bytes": 3058,
"path": "linuxcnc/src/emc/motion/usrmotintf.h",
"sha256": "1f5f6eb571ce0ea73084f0c8248182c3101359c855d6ca70ef46dde7ad94b3d3"
},
{
"bytes": 1259,
"path": "linuxcnc/src/emc/task/Submakefile",
"sha256": "c0f97c488c655acffeb91208d6ba0592a7252d33bf856cdacd37876ffaca78fb"
},
{
"bytes": 2725,
"path": "linuxcnc/src/emc/task/backtrace.cc",
"sha256": "07060b5eda02cead50c66edc20a2154fbd97cf432d27f98e31da08029c1875db"
},
{
"bytes": 126928,
"path": "linuxcnc/src/emc/task/emccanon.cc",
"sha256": "87bc78d9594e1d0042b28e75b0faa01f650b33c901887dbe686a0927b0eba699"
},
{
"bytes": 5337,
"path": "linuxcnc/src/emc/task/emcsvr.cc",
"sha256": "494cc0f5d5042c37ddd10da2f1e731b7c6c615027972a014ee4b9e6c7104482c"
},
{
"bytes": 19313,
"path": "linuxcnc/src/emc/task/emctask.cc",
"sha256": "db3996e57edbf042cc0ff85580ac62728491fb55e7236219a3a76bfbee6beee9"
},
{
"bytes": 105741,
"path": "linuxcnc/src/emc/task/emctaskmain.cc",
"sha256": "77490e65a1d3436086bedbfee68d6f467ca2d1a50139c64b3ace0dd4ed63022e"
},
{
"bytes": 4344,
"path": "linuxcnc/src/emc/task/signalhandler.cc",
"sha256": "fee333fda054449c7d49fed107b741d5ffd87eabc5881631e522ca68baf32288"
},
{
"bytes": 2194,
"path": "linuxcnc/src/emc/task/task.hh",
"sha256": "74a2c27fbf06d389d190ec8bca9d2163cca7645cec2fbc45761fc702f7441da8"
},
{
"bytes": 21110,
"path": "linuxcnc/src/emc/task/taskclass.cc",
"sha256": "837dbf1ca0fabed601baeca6a3785e6880330e763b2cfd363d121a5099935431"
},
{
"bytes": 2007,
"path": "linuxcnc/src/emc/task/taskclass.hh",
"sha256": "529f5d44ec108d3e523145a11da9fcf171486711ec98580e5067adaf8ee75799"
},
{
"bytes": 56981,
"path": "linuxcnc/src/emc/task/taskintf.cc",
"sha256": "017db5aeb5f6dcfbf138927b5114bdf4385e385309fd544f62275e6280a45834"
},
{
"bytes": 19270,
"path": "linuxcnc/src/emc/task/taskmodule.cc",
"sha256": "dd139359b01b3616e33dfa405203c2af4a854d2c71ebc2aa0943a3db46568216"
},
{
"bytes": 225,
"path": "linuxcnc/src/emc/tp/Submakefile",
"sha256": "8af41d532de2a2bbcc6ec0238c88547fbf14ad80a666fa1fc5c39d2c27d016fc"
},
{
"bytes": 58264,
"path": "linuxcnc/src/emc/tp/blendmath.c",
"sha256": "27b9b1192f273f77d52b7810dbd633e8ffd03f0e3127015eafae475e18b0666a"
},
{
"bytes": 9443,
"path": "linuxcnc/src/emc/tp/blendmath.h",
"sha256": "48276307d04edfaf63a394e41aa62f698e29787884d1cd23673f18763c4088f1"
},
{
"bytes": 136,
"path": "linuxcnc/src/emc/tp/meson.build",
"sha256": "1dba7d81a6cb02c6ed1dcd8d5e54f41a7c6bce3a46ddd95744c98409a4f6c6a7"
},
{
"bytes": 6293,
"path": "linuxcnc/src/emc/tp/spherical_arc.c",
"sha256": "58a31693ffc61528c4e8842386fcd0267ab28edef4a27b2aa91d1b7af85fc934"
},
{
"bytes": 2284,
"path": "linuxcnc/src/emc/tp/spherical_arc.h",
"sha256": "24369c82fd770cdfd392c8ae8a2e30a46e04b44a3ed6bbd8745d45bc85147801"
},
{
"bytes": 28016,
"path": "linuxcnc/src/emc/tp/tc.c",
"sha256": "ff2a123adaa198c71dcd9a9c1a3a403ab25f9d6c217684afc4a093829adef2c1"
},
{
"bytes": 3888,
"path": "linuxcnc/src/emc/tp/tc.h",
"sha256": "48caa2255744257b9b8e37812766f7feb3d012d1891dfdd6634edd32f1c65ba5"
},
{
"bytes": 6363,
"path": "linuxcnc/src/emc/tp/tc_types.h",
"sha256": "5ce27b5a545d8b4acaf119bf314fc4fed9320c22f975d786b7b2ec94d8029cb5"
},
{
"bytes": 8541,
"path": "linuxcnc/src/emc/tp/tcq.c",
"sha256": "2eb9de3c38d4d86338bda149baf52f9e9c87111cac9dff2b17884a2deb0feb05"
},
{
"bytes": 2181,
"path": "linuxcnc/src/emc/tp/tcq.h",
"sha256": "61f516f51a78f505fc2d2ee60c967de2c35dce298def5466e4b8a5a9aa7fcbd2"
},
{
"bytes": 121641,
"path": "linuxcnc/src/emc/tp/tp.c",
"sha256": "e0e4be6ef0b0a6c8b5eee32db9307986651938bc72c4e2c44750133b8da4dba8"
},
{
"bytes": 3534,
"path": "linuxcnc/src/emc/tp/tp.h",
"sha256": "e36449a34f0ae67fa15e570b3c3117d39d3bada44c9e1ba61e7aa52533c46f17"
},
{
"bytes": 1736,
"path": "linuxcnc/src/emc/tp/tp_debug.h",
"sha256": "033eab89f8378ed781b4c494ca4abba986282f83cb0ca7288bf56b5a5e4c6228"
},
{
"bytes": 4556,
"path": "linuxcnc/src/emc/tp/tp_types.h",
"sha256": "2d552b57e2ae7cd71dede51c0a1adc86a01173dc24d49d8bbdb8467e0a85d18d"
},
{
"bytes": 1025,
"path": "linuxcnc/src/emc/tp/tpmod.c",
"sha256": "1420f5777e0d1063709a6fdf4e391ec64abc43f9dd1da0838571052a92139d94"
},
{
"bytes": 48,
"path": "linuxcnc/unit_tests/tp/meson.build",
"sha256": "625c4249c225fed116bc998530cc539703c341cb3426851aedaae58f4291de3d"
},
{
"bytes": 2318,
"path": "linuxcnc/unit_tests/tp/test_blendmath.c",
"sha256": "0b2aaf0ec2506a4c2fd43a236f01c6c4e08fbf58998126926a8690a314d4d855"
}
],
"fingerprint_sha256": "4c8f89826279721c328be9b167798e59c811568a3b830dd2d1faf5d39b1c34e1",
"roots": [
"linuxcnc/src/emc/tp",
"linuxcnc/src/emc/motion",
"linuxcnc/src/emc/task",
"linuxcnc/unit_tests/tp"
]
},
"native_and_wasm": true,
"outputs": {
"bytes": 10996,
"file_count": 3,
"files": [
{
"bytes": 1819,
"path": "wasm-core/include/sim_motion_queue.h",
"sha256": "c26af2b5cff6cbc687f621d745601ff287eebd62011ea46fc7fa692b446a08cc"
},
{
"bytes": 7791,
"path": "wasm-core/src/sim_motion_queue.c",
"sha256": "56074e27df90bc1b60134eac3cdf84c78149388f99a19a027f6714e3784b04ed"
},
{
"bytes": 1386,
"path": "wasm-core/tests/sim_motion_queue_test.c",
"sha256": "882c90f9963b2433b868859c50a91dbdecef93454039b05b11241c1f76c13f31"
}
],
"fingerprint_sha256": "4c4afb2b2787d69c962edccf2db7dfc9a94a4e4bbf29ab095c409b7373d218cd"
},
"queue": {
"capacity": 64,
"continuity_tolerance": 1e-12,
"motion_types": [
1,
2,
3,
4
],
"tick": "fixed cycle_time"
},
"schema": "motion-runtime-report.v1",
"status": "passed"
}

View File

@@ -0,0 +1,248 @@
{
"configuration_candidate": {
"coordinates": "XYZAB",
"hal_geometry": {
"base": [
-418.0,
500.0,
-140.0,
418.0,
500.0,
-140.0,
0.0,
500.0,
-310.0,
418.0,
500.0,
510.0,
-418.0,
500.0,
510.0
],
"effector_radius": [
80.3,
80.3,
80.3,
80.3,
80.3
],
"effector_z": [
87.0,
114.5,
140.5,
232.5,
258.5
]
},
"hal_pins": [
"last-iterations",
"max-iterations",
"limit-iterations",
"max-error",
"convergence-criterion",
"tool-offset"
],
"joints": 5,
"kinematics": "pentakins",
"path": "linuxcnc/configs/sim/axis/pentapod/pentapod.ini",
"scan": true
},
"geometry": {
"orientation": "roll/pitch only; yaw fixed at zero",
"struts": 5,
"units": "configuration units; XYZ linear and AB degrees"
},
"implementation_status": "verified",
"mapping": {
"coordinates": "XYZAB",
"extra_joint_policy": "rejected",
"joint_semantics": [
"strut-0",
"strut-1",
"strut-2",
"strut-3",
"strut-4"
],
"kinematics_joint_count": 5
},
"native_wasm": {
"native_command": "build-native/sim_kinematics_test",
"status": "passed",
"wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"
},
"oracle": {
"blocked_samples": [
"nominal five-strut pose",
"negative-z seed",
"nonpositive strut",
"iteration limit",
"maximum error",
"singular Jacobian",
"joint limit"
],
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "b1d1cdc8fbd64b64ffa88023c77c7ce097952966353567b023c0667a8dc1be75",
"samples": {
"byte_identical_json": true,
"native": {
"boundaries": {
"alternate_branch": 0,
"alternate_pose": [
0.0,
0.0,
0.0
],
"alternate_status": -702,
"branch": 0,
"iteration_limit": -702,
"iterations": 1,
"limit_error": -702,
"max_error": -702,
"nonpositive": -702,
"residual": 0.0,
"singular": -704
},
"pentapod": {
"inverse": [
619.814203778,
655.78680467,
750.465994974,
351.035577947,
327.336726304
],
"iterations": 1,
"pose": [
0.0,
500.0,
300.0,
2.0,
-3.0
],
"residual": 0.0,
"state_hash": 10088895819155863850
}
},
"state_hash_identical": true,
"wasm": {
"boundaries": {
"alternate_branch": 0,
"alternate_pose": [
0.0,
0.0,
0.0
],
"alternate_status": -702,
"branch": 0,
"iteration_limit": -702,
"iterations": 1,
"limit_error": -702,
"max_error": -702,
"nonpositive": -702,
"residual": 0.0,
"singular": -704
},
"pentapod": {
"inverse": [
619.814203778,
655.78680467,
750.465994974,
351.035577947,
327.336726304
],
"iterations": 1,
"pose": [
0.0,
500.0,
300.0,
2.0,
-3.0
],
"residual": 0.0,
"state_hash": 10088895819155863850
}
}
},
"schema": "pentapod-kinematics-report.v1",
"solver": {
"alternate_seed_flag": "SIM_KIN_GENHEX_ALTERNATE_SEED bit 1; negative-z seed is explicitly bounded",
"default_convergence_tolerance": 1e-09,
"default_iteration_limit": 120,
"default_max_error": 1000.0,
"forward": "Newton-Raphson 5x5 numerical Jacobian",
"inverse": "closed-form radial/z strut lengths",
"seed": [
0.0,
500.0,
300.0,
0.0,
0.0
]
},
"source_records": {
"config": {
"path": "linuxcnc/configs/sim/axis/pentapod/pentapod.ini",
"sha256": "0fb210122ab482087d3a95e5e7487baecb84c3260dca1769970f4c19b2ad0618"
},
"hal": {
"path": "linuxcnc/configs/sim/axis/pentapod/kinematics.hal",
"sha256": "cf56ea1c1fe2b49d1af6a771bd73ea43ecc7a37991717b68a1e017c101bc6ae2"
},
"header": {
"path": "linuxcnc/src/emc/kinematics/pentakins.h",
"sha256": "af8551454177bbcc85e4e17377e059573ab8c15dc022de2a4be9b70dead74e2d"
},
"implementation": {
"path": "wasm-core/src/sim_kinematics.c",
"sha256": "424873c8e5d3d5b45daf616d95a58157c6dbcbbee9f4cc512b58bfb2672c6145"
},
"kinematics_header": {
"path": "wasm-core/include/sim_kinematics.h",
"sha256": "0c6d955717982e9d1a24d1240a1c657fc37dfe166c5738d3ee23878e46b2a98d"
},
"source": {
"path": "linuxcnc/src/emc/kinematics/pentakins.c",
"sha256": "1b385c2ab3a1bcb106d1d22a2dfe14519dba9c4a5df4cb31071a180845cda4c6"
},
"test": {
"path": "wasm-core/tests/sim_kinematics_test.c",
"sha256": "3bda403d3af592dcc69ee9b1f3c96dda97b952f827a1c9ce5393e01cd357c403"
}
},
"status": "partial",
"workspace_and_boundaries": {
"alternate_seed": [
0.0,
0.0,
0.0
],
"alternate_seed_error": -702,
"iteration_limit_error": -702,
"iterations": 1,
"joint_limit_error": -702,
"max_error_boundary": -702,
"nominal": {
"inverse": [
619.814203778,
655.78680467,
750.465994974,
351.035577947,
327.336726304
],
"iterations": 1,
"pose": [
0.0,
500.0,
300.0,
2.0,
-3.0
],
"residual": 0.0,
"state_hash": 10088895819155863850
},
"nonpositive_joint_error": -702,
"residual": 0.0,
"singular_jacobian_error": -704
}
}

View File

@@ -1,10 +1,230 @@
{ {
"schema": "posemath-runtime-report.v1", "adapter_operations": [
"status": "passed", "normalize",
"implementation": "linuxcnc/src/libnml/posemath", "dot",
"linked_sources": ["_posemath.c", "gomath.c", "sincos.c", "posemath.cc"], "line_point",
"adapter_operations": ["normalize", "dot", "line_point", "is_finite"], "pose_line_point",
"error_boundaries": ["nonfinite", "degenerate", "range"], "segment_validate",
"segment_point",
"is_finite"
],
"determinism": {
"byte_identical": true,
"runs": 2,
"state_hash_algorithm": "fnv1a64 over sim_tp_state object bytes",
"state_hash_identical": true
},
"error_boundaries": [
"argument",
"nonfinite",
"degenerate",
"range",
"motion_type",
"state",
"capacity"
],
"fixed_float": {
"contract": "off",
"fast_math": false,
"standard": "IEEE-754 binary64"
},
"golden_tolerance": "exact_for_adapter_inputs",
"implementation": "linuxcnc/src/libnml/posemath + linuxcnc/src/emc/tp",
"inputs": {
"bytes": 488358,
"file_count": 28,
"files": [
{
"bytes": 712,
"path": "linuxcnc/src/libnml/posemath/Submakefile",
"sha256": "ce99301b14a570f3a5a6832d214131cc5331d655b76a0078803112b1f229b6ac"
},
{
"bytes": 47948,
"path": "linuxcnc/src/libnml/posemath/_posemath.c",
"sha256": "b75ca68deef765090923cbbbd2fc8317d131c53c06fbbbc78b25ba331aeddd31"
},
{
"bytes": 89361,
"path": "linuxcnc/src/libnml/posemath/gomath.c",
"sha256": "0095605a866d95d3cfce0236e9479e477a276a884bb05b50a63a474f08bc2f12"
},
{
"bytes": 28115,
"path": "linuxcnc/src/libnml/posemath/gomath.h",
"sha256": "9a05762201ea93126a768b1bb717f8593786d591a38b17852565cb93103a9d85"
},
{
"bytes": 4700,
"path": "linuxcnc/src/libnml/posemath/gotypes.h",
"sha256": "977fd4907da53fa831d1f3e85ae41e8018fe8fbd2540db25f53dc83966e8dd91"
},
{
"bytes": 169,
"path": "linuxcnc/src/libnml/posemath/meson.build",
"sha256": "a48bc583c107d469ac6ec2df51b36b65caa5d8a476b906a4dbbba9d7d896ddf7"
},
{
"bytes": 23563,
"path": "linuxcnc/src/libnml/posemath/posemath.cc",
"sha256": "c83f52c26bd233d9403488f5f38df5121602d842948ad4dc2c37789e18c5f67f"
},
{
"bytes": 32215,
"path": "linuxcnc/src/libnml/posemath/posemath.h",
"sha256": "23205524123476e0a17da40983bdf0aad529684294884c5144f50380d190da69"
},
{
"bytes": 575,
"path": "linuxcnc/src/libnml/posemath/sincos.c",
"sha256": "69b2671ecbdaa6d3c771e4ef267d2fb8abbddeaa3bf6c08d02d4e222e5b2c8fe"
},
{
"bytes": 508,
"path": "linuxcnc/src/libnml/posemath/sincos.h",
"sha256": "1866f1647e0834c687e541f8f0540799026d6e9eeff97b7ec8dc5e6ec4438e08"
},
{
"bytes": 225,
"path": "linuxcnc/src/emc/tp/Submakefile",
"sha256": "8af41d532de2a2bbcc6ec0238c88547fbf14ad80a666fa1fc5c39d2c27d016fc"
},
{
"bytes": 58264,
"path": "linuxcnc/src/emc/tp/blendmath.c",
"sha256": "27b9b1192f273f77d52b7810dbd633e8ffd03f0e3127015eafae475e18b0666a"
},
{
"bytes": 9443,
"path": "linuxcnc/src/emc/tp/blendmath.h",
"sha256": "48276307d04edfaf63a394e41aa62f698e29787884d1cd23673f18763c4088f1"
},
{
"bytes": 136,
"path": "linuxcnc/src/emc/tp/meson.build",
"sha256": "1dba7d81a6cb02c6ed1dcd8d5e54f41a7c6bce3a46ddd95744c98409a4f6c6a7"
},
{
"bytes": 6293,
"path": "linuxcnc/src/emc/tp/spherical_arc.c",
"sha256": "58a31693ffc61528c4e8842386fcd0267ab28edef4a27b2aa91d1b7af85fc934"
},
{
"bytes": 2284,
"path": "linuxcnc/src/emc/tp/spherical_arc.h",
"sha256": "24369c82fd770cdfd392c8ae8a2e30a46e04b44a3ed6bbd8745d45bc85147801"
},
{
"bytes": 28016,
"path": "linuxcnc/src/emc/tp/tc.c",
"sha256": "ff2a123adaa198c71dcd9a9c1a3a403ab25f9d6c217684afc4a093829adef2c1"
},
{
"bytes": 3888,
"path": "linuxcnc/src/emc/tp/tc.h",
"sha256": "48caa2255744257b9b8e37812766f7feb3d012d1891dfdd6634edd32f1c65ba5"
},
{
"bytes": 6363,
"path": "linuxcnc/src/emc/tp/tc_types.h",
"sha256": "5ce27b5a545d8b4acaf119bf314fc4fed9320c22f975d786b7b2ec94d8029cb5"
},
{
"bytes": 8541,
"path": "linuxcnc/src/emc/tp/tcq.c",
"sha256": "2eb9de3c38d4d86338bda149baf52f9e9c87111cac9dff2b17884a2deb0feb05"
},
{
"bytes": 2181,
"path": "linuxcnc/src/emc/tp/tcq.h",
"sha256": "61f516f51a78f505fc2d2ee60c967de2c35dce298def5466e4b8a5a9aa7fcbd2"
},
{
"bytes": 121641,
"path": "linuxcnc/src/emc/tp/tp.c",
"sha256": "e0e4be6ef0b0a6c8b5eee32db9307986651938bc72c4e2c44750133b8da4dba8"
},
{
"bytes": 3534,
"path": "linuxcnc/src/emc/tp/tp.h",
"sha256": "e36449a34f0ae67fa15e570b3c3117d39d3bada44c9e1ba61e7aa52533c46f17"
},
{
"bytes": 1736,
"path": "linuxcnc/src/emc/tp/tp_debug.h",
"sha256": "033eab89f8378ed781b4c494ca4abba986282f83cb0ca7288bf56b5a5e4c6228"
},
{
"bytes": 4556,
"path": "linuxcnc/src/emc/tp/tp_types.h",
"sha256": "2d552b57e2ae7cd71dede51c0a1adc86a01173dc24d49d8bbdb8467e0a85d18d"
},
{
"bytes": 1025,
"path": "linuxcnc/src/emc/tp/tpmod.c",
"sha256": "1420f5777e0d1063709a6fdf4e391ec64abc43f9dd1da0838571052a92139d94"
},
{
"bytes": 48,
"path": "linuxcnc/unit_tests/tp/meson.build",
"sha256": "625c4249c225fed116bc998530cc539703c341cb3426851aedaae58f4291de3d"
},
{
"bytes": 2318,
"path": "linuxcnc/unit_tests/tp/test_blendmath.c",
"sha256": "0b2aaf0ec2506a4c2fd43a236f01c6c4e08fbf58998126926a8690a314d4d855"
}
],
"fingerprint_sha256": "bb921823ad630db52343b52fb1d8074c8e333ceedb0735949ff5a74596c98dc7",
"roots": [
"linuxcnc/src/libnml/posemath",
"linuxcnc/src/emc/tp",
"linuxcnc/unit_tests/tp"
]
},
"linked_sources": [
"_posemath.c",
"gomath.c",
"sincos.c",
"posemath.cc"
],
"motion_types": {
"circular": 2,
"linear": 1,
"rigid_tap": 3,
"spherical": 4
},
"native_and_wasm": true, "native_and_wasm": true,
"golden_tolerance": "exact_for_adapter_inputs" "outputs": {
"bytes": 19323,
"file_count": 3,
"files": [
{
"bytes": 3280,
"path": "wasm-core/include/sim_posemath.h",
"sha256": "fef257b09f68e5649f0acacfcd3590d699d6f678927fbf8db7d103a40bf7da62"
},
{
"bytes": 13491,
"path": "wasm-core/src/sim_posemath.c",
"sha256": "939b221ed6ede1229d26c170ab3514884d5bafc90854ab09db641925859b5e8d"
},
{
"bytes": 2552,
"path": "wasm-core/tests/sim_posemath_test.c",
"sha256": "751910f2bcd740c963b008e1ae4cb1dd058d2942ab1d63dd0a9f510a20a408d2"
}
],
"fingerprint_sha256": "7db9f98a48f6f945374d367bdb878348704db218a426e6baf926ec358b607bc1"
},
"schema": "posemath-runtime-report.v2",
"status": "passed",
"tp_operations": [
"init",
"reset",
"enqueue",
"step",
"read_state",
"state_hash"
]
} }

View File

@@ -0,0 +1,229 @@
{
"boundaries": {
"puma_unreachable_or_singular": "SIM_KIN_ERR_RANGE/SINGULAR verified",
"scara_unreachable": "SIM_KIN_ERR_RANGE verified",
"scorbot_unreachable": "SIM_KIN_ERR_RANGE verified",
"state_hash": "native/WASM identical after 1e-9 hash quantization"
},
"configuration_candidates": {
"puma": {
"coordinates": "XYZABC",
"joints": 6,
"kinematics": "pumakins",
"origin": "shipped",
"path": "linuxcnc/configs/sim/axis/vismach/puma/puma.ini",
"scan": true
},
"scara": {
"coordinates": "xyzcab",
"joints": 6,
"kinematics": "scarakins",
"origin": "shipped",
"path": "linuxcnc/configs/sim/axis/vismach/scara/scara.ini",
"scan": true
},
"scorbot": {
"coordinates": "XYZAB",
"joints": 5,
"kinematics": "scorbot-kins",
"origin": "workspace_derived_fixture; source has no shipped sim INI",
"path": "linuxcnc/configs/sim/axis/vismach/scorbot/scorbot.ini",
"scan": true
}
},
"geometry": {
"puma": {
"branches": [
"shoulder",
"elbow",
"wrist flip"
],
"parameters": {
"A2": 400.0,
"A3": 50.0,
"D3": 100.0,
"D4": 400.0,
"D6": 95.0
},
"singularity": "wrist t1/t2 threshold"
},
"scara": {
"branch": "elbow flag selects q1 sign",
"defaults": {
"D1": 490.0,
"D2": 340.0,
"D3": 50.0,
"D4": 250.0,
"D5": 50.0,
"D6": 50.0
},
"orientation": "C=a0+a1+a3"
},
"scorbot": {
"branch": "fixed elbow solution from source",
"controlled_point": "wrist J3/J4",
"links_mm": {
"L0_horizontal": 16.0,
"L0_vertical": 140.0,
"L1": 221.0,
"L2": 221.0
}
}
},
"implementation_status": "verified",
"native_wasm": {
"native_command": "build-native/sim_kinematics_test",
"status": "passed",
"wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"
},
"oracle": {
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "fad0d9058ca41776a07fd7b88d467b0d8f96135e036f0e887c9583c78a4f97ea",
"samples": {
"byte_identical_json": true,
"native": {
"puma": {
"inverse": [
10.0,
20.0,
-30.0,
40.0,
25.0,
-15.0
],
"pose": [
450.112320486,
207.114833215,
-612.180622409
],
"state_hash": 3161234248163340028
},
"scara": {
"inverse": [
-3.644300703,
40.0,
25.0,
-31.355699297,
5.0,
-3.0
],
"pose": [
590.460310444,
130.945742721,
465.0
],
"state_hash": 4515826253922604237
},
"scorbot": {
"inverse": [
25.0,
35.0,
-20.0,
10.0,
-5.0
],
"pose": [
366.786994761,
171.035584569,
191.173940759
],
"state_hash": 16037922903473574131
}
},
"wasm": {
"puma": {
"inverse": [
10.0,
20.0,
-30.0,
40.0,
25.0,
-15.0
],
"pose": [
450.112320486,
207.114833215,
-612.180622409
],
"state_hash": 3161234248163340028
},
"scara": {
"inverse": [
-3.644300703,
40.0,
25.0,
-31.355699297,
5.0,
-3.0
],
"pose": [
590.460310444,
130.945742721,
465.0
],
"state_hash": 4515826253922604237
},
"scorbot": {
"inverse": [
25.0,
35.0,
-20.0,
10.0,
-5.0
],
"pose": [
366.786994761,
171.035584569,
191.173940759
],
"state_hash": 16037922903473574131
}
}
},
"schema": "robot-kinematics-report.v1",
"source_records": {
"header": {
"path": "wasm-core/include/sim_kinematics.h",
"sha256": "db16293ec3645559a0be71a40963693f3d1e3f5fdedc92902be19b47f653dfa8"
},
"implementation": {
"path": "wasm-core/src/sim_kinematics.c",
"sha256": "7a77915c59f190bcbe5c93d8f53498745bfcf978c167ba8878a21b914f9401fd"
},
"puma_config": {
"path": "linuxcnc/configs/sim/axis/vismach/puma/puma.ini",
"sha256": "26ee23af27aa03d2da89b842992802ece2423195a5ac0960b28bb751871d74f6"
},
"puma_params": {
"path": "linuxcnc/configs/sim/axis/vismach/puma/puma_dh.hal",
"sha256": "b57992601bf614c26e53c8add3c3aac0a0df79929dd64f897a19630437d1c181"
},
"puma_source": {
"path": "linuxcnc/src/emc/kinematics/pumakins.c",
"sha256": "5d473d34beacd696fc34719658e1622a1228494fe6596e277f4c34a51dfb91b2"
},
"scara_config": {
"path": "linuxcnc/configs/sim/axis/vismach/scara/scara.ini",
"sha256": "602e02bb6d320b728d9ae15904a5d229ea1a31e932c18e9018b45be007d695ce"
},
"scara_source": {
"path": "linuxcnc/src/emc/kinematics/scarakins.c",
"sha256": "f6e37f7cf5b932fc27378ab9ed03ef71c2010aa38f883980a1b6c6b9265cf9f9"
},
"scorbot_config": {
"path": "linuxcnc/configs/sim/axis/vismach/scorbot/scorbot.ini",
"sha256": "4e8edd061a033d45e1e71c56bdd521ee10d3f60def3be020bbd437fc49271343"
},
"scorbot_source": {
"path": "linuxcnc/src/emc/kinematics/scorbot-kins.c",
"sha256": "89badd55ec168139143c58f67c64dbdb9e5b411c3b847404beaf79ba8c591b5d"
},
"test": {
"path": "wasm-core/tests/sim_kinematics_test.c",
"sha256": "ceeb8b4100d034d76e70ce675d78b2d62c51e8e70be2205a9c8ae02e105a6aa6"
}
},
"status": "partial"
}

View File

@@ -532,6 +532,8 @@
{"bytes":208,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc","sha256":"5b194542cca489e2f998104e7054c7d95433aa16f22be0ce5c09411089509b66","source_id":"config:linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc"} {"bytes":208,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc","sha256":"5b194542cca489e2f998104e7054c7d95433aa16f22be0ce5c09411089509b66","source_id":"config:linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc"}
{"bytes":207,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/rose_engine/rose_engine.halshow","sha256":"174d37a7fc858c6534bcdaa239b35d1d873540b37bee173cf2aee53b80a4212a","source_id":"config:linuxcnc/configs/sim/axis/rose_engine/rose_engine.halshow"} {"bytes":207,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/rose_engine/rose_engine.halshow","sha256":"174d37a7fc858c6534bcdaa239b35d1d873540b37bee173cf2aee53b80a4212a","source_id":"config:linuxcnc/configs/sim/axis/rose_engine/rose_engine.halshow"}
{"bytes":1694,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini","sha256":"0685e9ca8c05c903b715e9fed6ecd23cbac13911ed335b6d14e24eb235de25d3","source_id":"config:linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini"} {"bytes":1694,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini","sha256":"0685e9ca8c05c903b715e9fed6ecd23cbac13911ed335b6d14e24eb235de25d3","source_id":"config:linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini"}
{"bytes":343,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/rotate/rotatekins.hal","sha256":"e3c9ec02685f64fdb72bb9e561b3709b450f0da29eac9cca0f17541f843b0954","source_id":"config:linuxcnc/configs/sim/axis/rotate/rotatekins.hal"}
{"bytes":515,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/rotate/rotatekins.ini","sha256":"324af9a73d21b5f0c9adaf3c97e698422790d403586901f9e161b6020b1fb4fa","source_id":"config:linuxcnc/configs/sim/axis/rotate/rotatekins.ini"}
{"bytes":174,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/sim.tbl","sha256":"52411645e06c20cf0e6c469e544a4d17de07d03254a6cb784e2468704e0a4a9e","source_id":"config:linuxcnc/configs/sim/axis/sim.tbl"} {"bytes":174,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/sim.tbl","sha256":"52411645e06c20cf0e6c469e544a4d17de07d03254a6cb784e2468704e0a4a9e","source_id":"config:linuxcnc/configs/sim/axis/sim.tbl"}
{"bytes":523,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/sim_mm.tbl","sha256":"1816cbb02cc6585d86676bb2d88f85b10b439cf42212e0b4c144f3846a3dcf30","source_id":"config:linuxcnc/configs/sim/axis/sim_mm.tbl"} {"bytes":523,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/sim_mm.tbl","sha256":"1816cbb02cc6585d86676bb2d88f85b10b439cf42212e0b4c144f3846a3dcf30","source_id":"config:linuxcnc/configs/sim/axis/sim_mm.tbl"}
{"bytes":618,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/simpockets.tbl","sha256":"e81a30f74f1c91fcece180e607f7e2cd63468bf4d432cc5cad2bc65eb7152150","source_id":"config:linuxcnc/configs/sim/axis/simpockets.tbl"} {"bytes":618,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/simpockets.tbl","sha256":"e81a30f74f1c91fcece180e607f7e2cd63468bf4d432cc5cad2bc65eb7152150","source_id":"config:linuxcnc/configs/sim/axis/simpockets.tbl"}
@@ -736,6 +738,8 @@
{"bytes":3622,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/vismach/scara/scara.ini","sha256":"602e02bb6d320b728d9ae15904a5d229ea1a31e932c18e9018b45be007d695ce","source_id":"config:linuxcnc/configs/sim/axis/vismach/scara/scara.ini"} {"bytes":3622,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/vismach/scara/scara.ini","sha256":"602e02bb6d320b728d9ae15904a5d229ea1a31e932c18e9018b45be007d695ce","source_id":"config:linuxcnc/configs/sim/axis/vismach/scara/scara.ini"}
{"bytes":3589,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/vismach/scara/scara.xml","sha256":"4fc724d9bd969c84ce6d9e4c2c4e47a1c1ec67646044e1ca227b1745f9131734","source_id":"config:linuxcnc/configs/sim/axis/vismach/scara/scara.xml"} {"bytes":3589,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/vismach/scara/scara.xml","sha256":"4fc724d9bd969c84ce6d9e4c2c4e47a1c1ec67646044e1ca227b1745f9131734","source_id":"config:linuxcnc/configs/sim/axis/vismach/scara/scara.xml"}
{"bytes":828,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/vismach/scara/scara_postgui.hal","sha256":"77b7360cf05b79fa6cc15f1f686f444fe55360668ec21f3859703bcd2d2a5596","source_id":"config:linuxcnc/configs/sim/axis/vismach/scara/scara_postgui.hal"} {"bytes":828,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/vismach/scara/scara_postgui.hal","sha256":"77b7360cf05b79fa6cc15f1f686f444fe55360668ec21f3859703bcd2d2a5596","source_id":"config:linuxcnc/configs/sim/axis/vismach/scara/scara_postgui.hal"}
{"bytes":250,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/vismach/scorbot/scorbot.hal","sha256":"ef66253c80e77899df824f9317e9ba0e9292f0e0389ef8e7ec22ebb465563363","source_id":"config:linuxcnc/configs/sim/axis/vismach/scorbot/scorbot.hal"}
{"bytes":420,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/vismach/scorbot/scorbot.ini","sha256":"4e8edd061a033d45e1e71c56bdd521ee10d3f60def3be020bbd437fc49271343","source_id":"config:linuxcnc/configs/sim/axis/vismach/scorbot/scorbot.ini"}
{"bytes":193,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/wrapped_rotary/README","sha256":"5f9fd4cb6e25c0a7b940e92131b962e10d671a1f6e16afe528901e9f73ab48a2","source_id":"config:linuxcnc/configs/sim/axis/wrapped_rotary/README"} {"bytes":193,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/wrapped_rotary/README","sha256":"5f9fd4cb6e25c0a7b940e92131b962e10d671a1f6e16afe528901e9f73ab48a2","source_id":"config:linuxcnc/configs/sim/axis/wrapped_rotary/README"}
{"bytes":193,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/wrapped_rotary/README_es","sha256":"5f9fd4cb6e25c0a7b940e92131b962e10d671a1f6e16afe528901e9f73ab48a2","source_id":"config:linuxcnc/configs/sim/axis/wrapped_rotary/README_es"} {"bytes":193,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/wrapped_rotary/README_es","sha256":"5f9fd4cb6e25c0a7b940e92131b962e10d671a1f6e16afe528901e9f73ab48a2","source_id":"config:linuxcnc/configs/sim/axis/wrapped_rotary/README_es"}
{"bytes":918,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/wrapped_rotary/wrapped_rotary.hal","sha256":"34102c0b792c4ddf9d2122898513a316f990b91f38e3ede62f5a6fe6f40c04a1","source_id":"config:linuxcnc/configs/sim/axis/wrapped_rotary/wrapped_rotary.hal"} {"bytes":918,"kind":"simulation_config","path":"linuxcnc/configs/sim/axis/wrapped_rotary/wrapped_rotary.hal","sha256":"34102c0b792c4ddf9d2122898513a316f990b91f38e3ede62f5a6fe6f40c04a1","source_id":"config:linuxcnc/configs/sim/axis/wrapped_rotary/wrapped_rotary.hal"}
@@ -3207,21 +3211,32 @@
{"bytes":311,"kind":"documentation","path":"linuxcnc/docs/src/xhtml11-latexmath.conf","sha256":"d31ed1d15f9ac1232af1e58c05d83feedd9cd73f9fe543d589f9340fdf0ad719","source_id":"doc:linuxcnc/docs/src/xhtml11-latexmath.conf"} {"bytes":311,"kind":"documentation","path":"linuxcnc/docs/src/xhtml11-latexmath.conf","sha256":"d31ed1d15f9ac1232af1e58c05d83feedd9cd73f9fe543d589f9340fdf0ad719","source_id":"doc:linuxcnc/docs/src/xhtml11-latexmath.conf"}
{"bytes":269,"kind":"documentation","path":"linuxcnc/docs/src/xhtml11-links.conf","sha256":"debc4b462499fdd8286b3812c705e40267e53d87ae8062250e8c23cb7ad59afe","source_id":"doc:linuxcnc/docs/src/xhtml11-links.conf"} {"bytes":269,"kind":"documentation","path":"linuxcnc/docs/src/xhtml11-links.conf","sha256":"debc4b462499fdd8286b3812c705e40267e53d87ae8062250e8c23cb7ad59afe","source_id":"doc:linuxcnc/docs/src/xhtml11-links.conf"}
{"bytes":886,"kind":"documentation","path":"linuxcnc/docs/src/xhtml11.conf","sha256":"c0222c552624bf3b7695e2f5c5db797be0e6cddcd77092293fd5e15b579f2ba9","source_id":"doc:linuxcnc/docs/src/xhtml11.conf"} {"bytes":886,"kind":"documentation","path":"linuxcnc/docs/src/xhtml11.conf","sha256":"c0222c552624bf3b7695e2f5c5db797be0e6cddcd77092293fd5e15b579f2ba9","source_id":"doc:linuxcnc/docs/src/xhtml11.conf"}
{"bytes":3882,"kind":"web_wasm_build_or_test","path":"wasm-core/CMakeLists.txt","sha256":"c289bad75abc394faf08b90d5cebb14c5293b4a9b64249a4aefd317acca036ac","source_id":"implementation:wasm-core/CMakeLists.txt"} {"bytes":6793,"kind":"web_wasm_build_or_test","path":"wasm-core/CMakeLists.txt","sha256":"e9296a3983a531e4e6864d05e2e97c8dbf29e0dc9e45942ecc1a0a7222b3a660","source_id":"implementation:wasm-core/CMakeLists.txt"}
{"bytes":933,"kind":"web_wasm_interface","path":"wasm-core/include/sim_api.h","sha256":"9dda70ef64afe0b398a3de5342e6bdf545c331bbef24f112be2a45acec327227","source_id":"implementation:wasm-core/include/sim_api.h"} {"bytes":933,"kind":"web_wasm_interface","path":"wasm-core/include/sim_api.h","sha256":"9dda70ef64afe0b398a3de5342e6bdf545c331bbef24f112be2a45acec327227","source_id":"implementation:wasm-core/include/sim_api.h"}
{"bytes":1050,"kind":"web_wasm_interface","path":"wasm-core/include/sim_event.h","sha256":"5406dbf7fb6a80924835d967641c9ed22cc8c25a13b72b76ce3d235abb0ad45d","source_id":"implementation:wasm-core/include/sim_event.h"} {"bytes":1050,"kind":"web_wasm_interface","path":"wasm-core/include/sim_event.h","sha256":"5406dbf7fb6a80924835d967641c9ed22cc8c25a13b72b76ce3d235abb0ad45d","source_id":"implementation:wasm-core/include/sim_event.h"}
{"bytes":1669,"kind":"web_wasm_interface","path":"wasm-core/include/sim_hal.h","sha256":"f87fc7fb47a848516e1e9f41298ceb1a7af18d022619c9336cc390aebb0f9ab2","source_id":"implementation:wasm-core/include/sim_hal.h"} {"bytes":1967,"kind":"web_wasm_interface","path":"wasm-core/include/sim_hal.h","sha256":"f0b25fda1c267ed8078469c9966fa3b09721eba77c575affea77d489437a3e49","source_id":"implementation:wasm-core/include/sim_hal.h"}
{"bytes":928,"kind":"web_wasm_interface","path":"wasm-core/include/sim_posemath.h","sha256":"71b092379fbfbbd46cf2280fb825fabe84219dd9a0fc1e16f0bf7232160dff05","source_id":"implementation:wasm-core/include/sim_posemath.h"} {"bytes":1872,"kind":"web_wasm_interface","path":"wasm-core/include/sim_interpreter.h","sha256":"7bf24098340e3da2272061acf7a43376c6c40fe9dffc55d012e3af65ceffedbb","source_id":"implementation:wasm-core/include/sim_interpreter.h"}
{"bytes":5446,"kind":"web_wasm_interface","path":"wasm-core/include/sim_kinematics.h","sha256":"939d4088d9e7120e4a405be154eb3d98cd4e5925fc27d47641c115624c1bd994","source_id":"implementation:wasm-core/include/sim_kinematics.h"}
{"bytes":1819,"kind":"web_wasm_interface","path":"wasm-core/include/sim_motion_queue.h","sha256":"c26af2b5cff6cbc687f621d745601ff287eebd62011ea46fc7fa692b446a08cc","source_id":"implementation:wasm-core/include/sim_motion_queue.h"}
{"bytes":3280,"kind":"web_wasm_interface","path":"wasm-core/include/sim_posemath.h","sha256":"fef257b09f68e5649f0acacfcd3590d699d6f678927fbf8db7d103a40bf7da62","source_id":"implementation:wasm-core/include/sim_posemath.h"}
{"bytes":1238,"kind":"web_wasm_interface","path":"wasm-core/include/sim_protocol.h","sha256":"d1dd3247159f0950a9aa2d903d25cfa184bccff1d7512de293868763b4035d35","source_id":"implementation:wasm-core/include/sim_protocol.h"} {"bytes":1238,"kind":"web_wasm_interface","path":"wasm-core/include/sim_protocol.h","sha256":"d1dd3247159f0950a9aa2d903d25cfa184bccff1d7512de293868763b4035d35","source_id":"implementation:wasm-core/include/sim_protocol.h"}
{"bytes":2262,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_api.c","sha256":"072ffb5aa13c4924c638464bb8487e5c293db589a9735c4d39c3a6d9455a3e59","source_id":"implementation:wasm-core/src/sim_api.c"} {"bytes":2262,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_api.c","sha256":"072ffb5aa13c4924c638464bb8487e5c293db589a9735c4d39c3a6d9455a3e59","source_id":"implementation:wasm-core/src/sim_api.c"}
{"bytes":2579,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_event.c","sha256":"a0af4a75c9c3b71d4a59df675b64f313eda05f4589538fe999cd92607d104397","source_id":"implementation:wasm-core/src/sim_event.c"} {"bytes":2579,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_event.c","sha256":"a0af4a75c9c3b71d4a59df675b64f313eda05f4589538fe999cd92607d104397","source_id":"implementation:wasm-core/src/sim_event.c"}
{"bytes":6029,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_hal.c","sha256":"5926e2cdc785d060bc199b2bdfa550c898facf8221204487fdc3ae841400a02e","source_id":"implementation:wasm-core/src/sim_hal.c"} {"bytes":9413,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_hal.c","sha256":"76a057064546a8ee9ba1098952755a4cdc56e6e714f2761b8a1826898d3ae621","source_id":"implementation:wasm-core/src/sim_hal.c"}
{"bytes":2449,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_posemath.c","sha256":"274445935ffc1bf7201879d0779c19e73a6eea70a19b10c5929208f77a6bde65","source_id":"implementation:wasm-core/src/sim_posemath.c"} {"bytes":11585,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_interpreter.c","sha256":"9f9f45f7c123be774dcf55a752a29e6cc1f873a1605cf89c4caa4f4ab125e6a4","source_id":"implementation:wasm-core/src/sim_interpreter.c"}
{"bytes":66830,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_kinematics.c","sha256":"6abae835c16feb27ec1dc28a9ea5384f0bc5190217ba04a4997048d83066b905","source_id":"implementation:wasm-core/src/sim_kinematics.c"}
{"bytes":7791,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_motion_queue.c","sha256":"56074e27df90bc1b60134eac3cdf84c78149388f99a19a027f6714e3784b04ed","source_id":"implementation:wasm-core/src/sim_motion_queue.c"}
{"bytes":13491,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_posemath.c","sha256":"939b221ed6ede1229d26c170ab3514884d5bafc90854ab09db641925859b5e8d","source_id":"implementation:wasm-core/src/sim_posemath.c"}
{"bytes":4302,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_protocol.c","sha256":"da63b458f64f997251323fc8b53a9c84d1f978f5e8ea5ed7c8dd6f5bceab60e4","source_id":"implementation:wasm-core/src/sim_protocol.c"} {"bytes":4302,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_protocol.c","sha256":"da63b458f64f997251323fc8b53a9c84d1f978f5e8ea5ed7c8dd6f5bceab60e4","source_id":"implementation:wasm-core/src/sim_protocol.c"}
{"bytes":858,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_api_test.c","sha256":"5fe8aa04543be2943c9e577bd6ce8f9ae9a8c98c7c24e84fd589b761fe39170c","source_id":"implementation:wasm-core/tests/sim_api_test.c"} {"bytes":858,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_api_test.c","sha256":"5fe8aa04543be2943c9e577bd6ce8f9ae9a8c98c7c24e84fd589b761fe39170c","source_id":"implementation:wasm-core/tests/sim_api_test.c"}
{"bytes":1178,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_event_test.c","sha256":"57fdb682dcb18632216b4093e6dfadf2736a0ab11a002438e3c269d4d4b5406d","source_id":"implementation:wasm-core/tests/sim_event_test.c"} {"bytes":1178,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_event_test.c","sha256":"57fdb682dcb18632216b4093e6dfadf2736a0ab11a002438e3c269d4d4b5406d","source_id":"implementation:wasm-core/tests/sim_event_test.c"}
{"bytes":1358,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_hal_test.c","sha256":"9dd08ea73df45d1d9cef14bb02138269a5b3e0a0e96de75c621f5c79f00318b9","source_id":"implementation:wasm-core/tests/sim_hal_test.c"} {"bytes":3505,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_hal_test.c","sha256":"5044ed422b37499291ff85fd7c601acc6665621cf33e9ff4d26f0e8fd5b7d533","source_id":"implementation:wasm-core/tests/sim_hal_test.c"}
{"bytes":972,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_posemath_test.c","sha256":"419176eef28697eecdac1e2e666dc42da491e600b966a559bfe9e5b60a2d0c0a","source_id":"implementation:wasm-core/tests/sim_posemath_test.c"} {"bytes":79,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_hal_test_main.c","sha256":"86f625eaa0c6d6a195b97c0d4df17af6c57f5407bcec0a830e7521cb9823165a","source_id":"implementation:wasm-core/tests/sim_hal_test_main.c"}
{"bytes":2066,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_interpreter_test.c","sha256":"c4fc3ba11b751ed9f3f770ff1ff164ce18aa19986003cf85d17a26de7aecd3ce","source_id":"implementation:wasm-core/tests/sim_interpreter_test.c"}
{"bytes":37392,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_kinematics_test.c","sha256":"c9e4d20973c6a487787504bd5de9a73829f53d8bbfd35f91e72c6c62c76f191e","source_id":"implementation:wasm-core/tests/sim_kinematics_test.c"}
{"bytes":93,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_kinematics_test_main.c","sha256":"e567fe412145d3c9b04afc91d6efc6a53c3af3b3b52b7dda860cc289d42dbfc0","source_id":"implementation:wasm-core/tests/sim_kinematics_test_main.c"}
{"bytes":1386,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_motion_queue_test.c","sha256":"882c90f9963b2433b868859c50a91dbdecef93454039b05b11241c1f76c13f31","source_id":"implementation:wasm-core/tests/sim_motion_queue_test.c"}
{"bytes":2552,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_posemath_test.c","sha256":"751910f2bcd740c963b008e1ae4cb1dd058d2942ab1d63dd0a9f510a20a408d2","source_id":"implementation:wasm-core/tests/sim_posemath_test.c"}
{"bytes":1457,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_protocol_test.c","sha256":"74c855e4bcae2091913847e2ba46ea5f0ae482229b53199baee5bbdcd3c0ef49","source_id":"implementation:wasm-core/tests/sim_protocol_test.c"} {"bytes":1457,"kind":"web_wasm_implementation","path":"wasm-core/tests/sim_protocol_test.c","sha256":"74c855e4bcae2091913847e2ba46ea5f0ae482229b53199baee5bbdcd3c0ef49","source_id":"implementation:wasm-core/tests/sim_protocol_test.c"}
{"bytes":1779,"kind":"web_wasm_implementation","path":"web/worker/capabilities.mjs","sha256":"175cb4dac10cc6b2eb80ed1cf24405151ae3fac6ff82f68e5e2e241d79f10c97","source_id":"implementation:web/worker/capabilities.mjs"} {"bytes":1779,"kind":"web_wasm_implementation","path":"web/worker/capabilities.mjs","sha256":"175cb4dac10cc6b2eb80ed1cf24405151ae3fac6ff82f68e5e2e241d79f10c97","source_id":"implementation:web/worker/capabilities.mjs"}
{"bytes":3084,"kind":"web_wasm_implementation","path":"web/worker/error-map.mjs","sha256":"83ca215534eed48a9082cc07230507c7b38cbb99f9f2a52e7026b5d49da1d167","source_id":"implementation:web/worker/error-map.mjs"} {"bytes":3084,"kind":"web_wasm_implementation","path":"web/worker/error-map.mjs","sha256":"83ca215534eed48a9082cc07230507c7b38cbb99f9f2a52e7026b5d49da1d167","source_id":"implementation:web/worker/error-map.mjs"}

View File

@@ -0,0 +1,197 @@
{
"configuration_candidate": {
"coordinates": "XYZ",
"hal_library": "linuxcnc/lib/hallib/tripodsim.hal",
"joints": 3,
"kinematics": "tripodkins",
"path": "linuxcnc/configs/sim/tklinuxcnc/tripod.ini",
"scan": true
},
"geometry": {
"base_A": [
0.0,
0.0,
0.0
],
"base_B": [
10.0,
0.0,
0.0
],
"base_C": [
5.0,
7.071,
0.0
],
"hal_parameters": {
"Bx": 10.0,
"Cx": 5.0,
"Cy": 7.071
},
"joint_semantics": [
"AD",
"BD",
"CD"
]
},
"implementation_status": "verified",
"mapping": {
"axis_joint_mapping": {
"X": 0,
"Y": 1,
"Z": 2
},
"coordinates": "XYZ",
"home_pose": [
0.0,
0.0,
1.0
],
"kinematics_joint_count": 3
},
"native_wasm": {
"native_command": "build-native/sim_kinematics_test",
"status": "passed",
"wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"
},
"oracle": {
"blocked_samples": [
"nominal above-root",
"alternate below-root",
"triangle-inequality no-solution",
"base-plane singular",
"joint-limit",
"degenerate base geometry"
],
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "e5fe7526d1dcff1f8327c1389809affb5ac4cf1913e4b428ec4420e6ae957070",
"samples": {
"byte_identical_json": true,
"native": {
"boundaries": {
"alternate_branch": 1,
"alternate_pose": [
2.0,
3.0,
-4.0
],
"alternate_status": 0,
"branch": 0,
"geometry_error": -703,
"iterations": 0,
"limit_error": -702,
"no_solution": -702,
"singular": -704
},
"tripod": {
"inverse": [
5.385164807,
9.433981132,
6.447715952
],
"pose": [
2.0,
3.0,
4.0
],
"state_hash": 7469146853106540736
}
},
"state_hash_identical": true,
"wasm": {
"boundaries": {
"alternate_branch": 1,
"alternate_pose": [
2.0,
3.0,
-4.0
],
"alternate_status": 0,
"branch": 0,
"geometry_error": -703,
"iterations": 0,
"limit_error": -702,
"no_solution": -702,
"singular": -704
},
"tripod": {
"inverse": [
5.385164807,
9.433981132,
6.447715952
],
"pose": [
2.0,
3.0,
4.0
],
"state_hash": 7469146853106540736
}
}
},
"schema": "tripod-kinematics-report.v1",
"solver": {
"alternate_branch": "Dz<0 when SIM_KIN_TRIPOD_BELOW_ROOT is set",
"convergence_tolerance": 0.0,
"default_branch": "Dz>0",
"forward": "analytic trilateration",
"inverse": "three Euclidean strut lengths",
"iterations": 0
},
"source_records": {
"config": {
"path": "linuxcnc/configs/sim/tklinuxcnc/tripod.ini",
"sha256": "8da7280a981d80e9b7d4b608a357fc87dbb9d83535636794e1ae97d5ebcb8f01"
},
"hal": {
"path": "linuxcnc/lib/hallib/tripodsim.hal",
"sha256": "e8b341253823f5e2764b2e7c4cc630b557b89dad5e1f7698bbddc1d0b9679747"
},
"header": {
"path": "wasm-core/include/sim_kinematics.h",
"sha256": "43e588f7c45c703970598e8e42ff5aae4b97a437eaff1d4cd6ad156bd204edc7"
},
"implementation": {
"path": "wasm-core/src/sim_kinematics.c",
"sha256": "f15842a1dbf0cb33f071ee1e70715a85a1f3dec44c4facc220edd924f6ca352c"
},
"source": {
"path": "linuxcnc/src/emc/kinematics/tripodkins.c",
"sha256": "0fb0876d2ab190e3bde69d3fe1ce0cebbb54adee2ddb4b05b929d7ca5cb3f31f"
},
"test": {
"path": "wasm-core/tests/sim_kinematics_test.c",
"sha256": "00d9efc3c701ba2848535357f6027e6d9d4d19796a17be96e290ddc0babe2c5c"
}
},
"status": "partial",
"workspace_and_boundaries": {
"alternate_branch": 1,
"alternate_pose": [
2.0,
3.0,
-4.0
],
"base_plane_singular_error": -704,
"geometry_error": -703,
"iterations": 0,
"joint_limit_error": -702,
"no_solution_error": -702,
"nominal": {
"inverse": [
5.385164807,
9.433981132,
6.447715952
],
"pose": [
2.0,
3.0,
4.0
],
"state_hash": 7469146853106540736
},
"root_branch": 0
}
}

View File

@@ -0,0 +1,307 @@
{
"abi_contract": "coverage/kinematics-abi-contract.json",
"configuration_ids": [
"config:linuxcnc/configs/sim/axis/anglejog/anglejog.ini:54:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/axis.ini:144:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/axis_9axis.ini:67:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/axis_mm.ini:136:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/canterp.ini:47:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/classicladder/demo_sim_cl.ini:52:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini:10:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/db_demo/base.inc:38:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/external_offsets/dynamic_offsets.ini:46:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/external_offsets/eoffsets.ini:67:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/external_offsets/jwp_z.ini:52:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/external_offsets/opa.ini:56:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/extrajoints/12extrajoints.ini:19:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/extrajoints/1extrajoint.ini:22:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/extrajoints/2extrajoints.ini:25:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/foam/axis_foam.ini:64:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/gantry/gantry.ini:3:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/gantry/gantry_jjog.ini:6:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/gantry/gantry_mm.ini:45:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/geometry/xyzc.ini:33:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/geometry/xyzc_origin.ini:34:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/gladevcp/gladevcp_panel.ini:69:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/gladevcp/gladevcp_panel_tester.ini:69:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/gladevcp/gladevcp_tab.ini:73:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/gladevcp/probe.ini:69:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/gladevcp/set-param-from-ini.ini:68:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/halui_pyvcp/halui.ini:57:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/histogram_demo.ini:67:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/historical_lathe.ini:68:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ini_hal_demo.ini:71:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ini_with_includes/kins.inc:2:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/iocontrolv2/axis-iocontrolv2-demo.ini:88:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/iocontrolv2/g8812.ini:135:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/iocontrolv2/m66track.ini:116:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/iocontrolv2/owordm6-ui-ns.ini:118:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/iocontrolv2/owordm6-ui.ini:98:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ja_tests/xyzb_locking_indexer/xyyzb.ini:20:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ja_tests/xyzb_locking_indexer/xyzb.ini:13:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ja_tests/xyzx_mpg/xyzx_base.inc:7:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ja_tests/xz/xz.ini:11:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ja_tests/xz/xz_both.ini:10:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto.ini:10:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/lathe-fanucy/lathe-fanucy.ini:62:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/lathe.ini:64:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/lathe_multispindle/lathe.ini:142:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/minimal_xyz.ini:39:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/moveoff/moveoff_base.inc:40:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ngcgui/ngcgui.ini:101:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ngcgui/ngcgui_gcmc.ini:106:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ngcgui/ngcgui_lathe.ini:90:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ngcgui/ngcgui_simple.ini:89:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/ngcgui/pyngcgui_axis.ini:102:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/orphans/acc_test.ini:66:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/orphans/axis_noio.ini:117:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/orphans/iocontrol-removed/core_sim_test.hal:5:loadrt:trivkins",
"config:linuxcnc/configs/sim/axis/orphans/iocontrol-removed/iocontrol-removed.ini:212:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/orphans/medium.ini:65:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/panelui-demo/panelui-demo.ini:67:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/plasma/imperial.ini:61:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/plasma/metric.ini:61:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/profile_axis.ini:64:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/qtvcp/qtvcp_tab.ini:74:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/qtvcp/qtvismach_tab.ini:73:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/random_tc.ini:64:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/remap/cycle/cycle.ini:75:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/remap/extend-builtins/extend-builtins.ini:105:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/remap/getting-started/demo.ini:156:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/remap/manual-toolchange-with-tool-length-switch/manualtoolchange.ini:92:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/remap/rack-toolchange/racktoolchange.ini:92:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/remap/stop-lookahead/demo.ini:73:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/simtcl/twopass_base.inc:51:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/spindle_orient/orient.ini:72:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/twopass/twopass.ini:74:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini:159:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/vismach/5axis/max5/max5kins.ini:4:kinematics:maxkins",
"config:linuxcnc/configs/sim/axis/vismach/5axis/max5/max5triv.ini:4:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/vismach/VMC_toolchange/vmc_graycode.ini:54:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/vismach/VMC_toolchange/vmc_index.ini:54:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/vismach/hbm/hbm.ini:49:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/vismach/rolfmill/rolfmill.hal:5:loadrt:trivkins",
"config:linuxcnc/configs/sim/axis/wrapped_rotary/wrapped_rotary.ini:45:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/xhc-hb04/inch_trivkins.inc:13:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/xhc-hb04/mm_trivkins.inc:13:kinematics:trivkins",
"config:linuxcnc/configs/sim/craftsman/craftsmancnc.ini:82:kinematics:trivkins",
"config:linuxcnc/configs/sim/craftsman/sim_core.hal:5:loadrt:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/6_axis.ini:72:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/7_axis.ini:72:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/7_axis_tool.ini:72:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/8_axis.ini:71:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/9_axis.ini:72:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/core_sim_6.hal:5:loadrt:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/core_sim_7.hal:5:loadrt:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/core_sim_8.hal:5:loadrt:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/core_sim_9.hal:5:loadrt:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy.ini:101:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_4_axis.ini:89:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini:121:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAC.ini:111:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZCW.ini:107:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_left_panel.ini:98:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_messages.ini:131:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_pedant.ini:99:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_plasma/gmoccapy_plasma.ini:113:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_pyngcgui.ini:134:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_right_panel.ini:94:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_sim_hardware_button.ini:102:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_tool_sensor.ini:128:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/gmoccapy_with_user_tabs.ini:119:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/lathe_configs/lathe.ini:95:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/lathe_configs/lathe_C.ini:96:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/lathe_configs/lathe_CW.ini:96:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/lathe_configs/lathe_backtool.ini:96:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/lathe_configs/lathe_imperial.ini:95:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/lathe_configs/lathe_macros.ini:101:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/Gantry/gantry_mm.ini:55:kinematics:trivkins",
"config:linuxcnc/configs/sim/gmoccapy/plasma_config/gmoccapy_plasma.ini:109:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/gscreen.ini:166:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/gscreen_custom/9axis.ini:135:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/gscreen_custom/gaxis_no_plot.ini:133:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/gscreen_custom/gscreen_gaxis.ini:133:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/gscreen_custom/industrial.ini:135:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/gscreen_custom/tester.ini:124:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/gscreen_custom/tester_metric.ini:137:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/gscreen_lathe.ini:65:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/gscreen_mm.ini:138:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/industrial_lathe_wear/industrial_lathe_wear.ini:79:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/ngcgui/pyngcgui_gscreen.ini:114:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/ngcgui/pyngcgui_gscreen_popupkeyboard.ini:111:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/silverdragon/silverdragon.ini:128:kinematics:trivkins",
"config:linuxcnc/configs/sim/gscreen/spartan/spartan.ini:67:kinematics:trivkins",
"config:linuxcnc/configs/sim/pyvcp_demo/pyvcp_bottom_panel.ini:153:kinematics:trivkins",
"config:linuxcnc/configs/sim/pyvcp_demo/pyvcp_demo1.ini:150:kinematics:trivkins",
"config:linuxcnc/configs/sim/pyvcp_demo/pyvcp_embed_tab.ini:154:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtaxis/gantry/qt_gantry.ini:3:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtaxis/xyz/qtaxis.ini:217:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/qtdragon_xyyz.ini:84:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/qtdragon_xyzy.ini:84:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/qtdragon_xyzya.ini:88:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_tool_probe/qtdragon_auto_tool_probe.ini:86:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_xyz/qtdragon_inch.ini:114:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_xyz/qtdragon_metric.ini:111:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_xyz/qtdragon_xyza.ini:89:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/qtdragon_xyza.ini:88:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/qtdragon_xyzab.ini:88:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/qtdragon_xyzac.ini:88:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/qtdragon_hd_xyz.ini:84:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/qtdragon_hd_xyza.ini:88:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/qtdragon_hd_xyzab.ini:88:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/qtdragon_hd_xyzac.ini:88:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_z_compensation/qtdragon_hd_z_compensation.ini:84:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtplasmac/qtplasmac_imperial_ini.inc:44:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtplasmac/qtplasmac_metric_ini.inc:44:kinematics:trivkins",
"config:linuxcnc/configs/sim/qttouchy/qttouchy.ini:198:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/blender.ini:207:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/industrial_lathe_wear/industrial_lathe_wear.ini:86:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qt_800x600.ini:164:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qt_gantry.ini:3:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtaxis.ini:220:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtaxis_lathe/qtaxis_lathe.ini:191:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtaxis_mpg.ini:224:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdefault.ini:210:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdefault_9axis.ini:73:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdefault_gantry.ini:3:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdefault_mm.ini:134:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon.ini:80:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_auto_tool_probe.ini:55:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_hd.ini:58:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_inch.ini:75:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_xyyz.ini:61:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_xyz.ini:62:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_xyza.ini:63:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_xyzy.ini:61:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_xyzya.ini:63:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtvcp_custom/qtdefault_ngc.ini:229:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtvcp_experimental/gremlin.ini:232:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtvcp_experimental/qtdefault_gladevcp.ini:232:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtvcp_experimental/qtdefault_ngc.ini:229:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/qtx1mill.ini:214:kinematics:trivkins",
"config:linuxcnc/configs/sim/qtvcp_screens/tester.ini:184:kinematics:trivkins",
"config:linuxcnc/configs/sim/tklinuxcnc/servo_sim.ini:49:kinematics:trivkins",
"config:linuxcnc/configs/sim/tklinuxcnc/tklinuxcnc.ini:79:kinematics:trivkins",
"config:linuxcnc/configs/sim/tklinuxcnc/trivkins/xz.ini:11:kinematics:trivkins",
"config:linuxcnc/configs/sim/tklinuxcnc/trivkins/xz_both.ini:10:kinematics:trivkins",
"config:linuxcnc/configs/sim/touchy/gladevcp/gladevcp_touchy.ini:90:kinematics:trivkins",
"config:linuxcnc/configs/sim/touchy/gladevcp/touchy.ini:86:kinematics:trivkins",
"config:linuxcnc/configs/sim/touchy/ngcgui/pyngcgui_touchy.ini:73:kinematics:trivkins",
"config:linuxcnc/configs/sim/touchy/ngcgui/pyngcgui_touchy_lathe.ini:88:kinematics:trivkins",
"config:linuxcnc/configs/sim/touchy/ngcgui/pyngcgui_touchy_moveoff.ini:75:kinematics:trivkins",
"config:linuxcnc/configs/sim/touchy/touchy.ini:64:kinematics:trivkins",
"config:linuxcnc/configs/sim/woodpecker/woodpecker.ini:49:kinematics:trivkins",
"config:linuxcnc/configs/sim/woodpecker/woodpecker_auto_tool_probe.ini:63:kinematics:trivkins",
"config:linuxcnc/configs/sim/woodpecker/woodpecker_xyz.ini:56:kinematics:trivkins",
"config:linuxcnc/configs/sim/woodpecker/woodpecker_xyza.ini:86:kinematics:trivkins",
"config:linuxcnc/configs/sim/woodpecker/woodpecker_xyzab.ini:74:kinematics:trivkins",
"config:linuxcnc/configs/sim/woodpecker/woodpecker_xyzy.ini:55:kinematics:trivkins",
"config:linuxcnc/configs/sim/woodpecker/woodpecker_xyzya.ini:58:kinematics:trivkins",
"config:linuxcnc/tests/abort/feed-rate/test.ini:59:kinematics:trivkins",
"config:linuxcnc/tests/abort/g64/test.ini:60:kinematics:trivkins",
"config:linuxcnc/tests/abort/on_abort_command-crazy-move/test.ini:67:kinematics:trivkins",
"config:linuxcnc/tests/abort/stop-button-crazy-move/test.ini:65:kinematics:trivkins",
"config:linuxcnc/tests/ccomp/early-exit/test.ini:58:kinematics:trivkins",
"config:linuxcnc/tests/classicladder/estop/classicladder-estop.ini:38:kinematics:trivkins",
"config:linuxcnc/tests/halui/jogging/halui.ini:60:kinematics:trivkins",
"config:linuxcnc/tests/halui/mdi/halui.ini:49:kinematics:trivkins",
"config:linuxcnc/tests/hard-limits/hard-limits.ini:37:kinematics:trivkins",
"config:linuxcnc/tests/interp/m98m99/12-M99-endless-main-program/motion-logger.ini:44:kinematics:trivkins",
"config:linuxcnc/tests/interp/mdi-oword-m66/interp.ini:47:kinematics:trivkins",
"config:linuxcnc/tests/interp/oword-mdi-sub-update/interp.ini:41:kinematics:trivkins",
"config:linuxcnc/tests/interp/subroutine-return/interp.ini:41:kinematics:trivkins",
"config:linuxcnc/tests/io-startup/nonrandom/test.ini:44:kinematics:trivkins",
"config:linuxcnc/tests/io-startup/random/test.ini:43:kinematics:trivkins",
"config:linuxcnc/tests/lathe/lathe.ini:47:kinematics:trivkins",
"config:linuxcnc/tests/linuxcncrsh-tcp/linuxcncrsh-test.ini:42:kinematics:trivkins",
"config:linuxcnc/tests/linuxcncrsh/linuxcncrsh-test.ini:58:kinematics:trivkins",
"config:linuxcnc/tests/mdi-queue-length/test.ini:45:kinematics:trivkins",
"config:linuxcnc/tests/mdi-queue/linuxcncrsh-test.ini:60:kinematics:trivkins",
"config:linuxcnc/tests/mdi-while-queuebuster-waitflag/test.ini:46:kinematics:trivkins",
"config:linuxcnc/tests/motion-logger/basic/test.ini:42:kinematics:trivkins",
"config:linuxcnc/tests/motion-logger/mountaindew/mountaindew.ini:45:kinematics:trivkins",
"config:linuxcnc/tests/motion-logger/startup-gcode-abort/test.ini:57:kinematics:trivkins",
"config:linuxcnc/tests/motion/g0/motion-test.ini:65:kinematics:trivkins",
"config:linuxcnc/tests/motion/jogwheel-axis/motion-test.ini:42:kinematics:trivkins",
"config:linuxcnc/tests/motion/jogwheel-joint/motion-test.ini:42:kinematics:trivkins",
"config:linuxcnc/tests/mqtt/simulator.ini:52:kinematics:trivkins",
"config:linuxcnc/tests/remap/remap-io/test-ngc.ini:64:kinematics:trivkins",
"config:linuxcnc/tests/remap/remap-io/test-py.ini:64:kinematics:trivkins",
"config:linuxcnc/tests/remap/remap-reentry/test.ini:45:kinematics:trivkins",
"config:linuxcnc/tests/rs274ngc-startup/test.ini:57:kinematics:trivkins",
"config:linuxcnc/tests/single-step/test.ini:43:kinematics:trivkins",
"config:linuxcnc/tests/startup-state/test.ini:57:kinematics:trivkins",
"config:linuxcnc/tests/statbuffer-g5x-abort/test.ini:56:kinematics:trivkins",
"config:linuxcnc/tests/t0/nonrandom/sim.ini:67:kinematics:trivkins",
"config:linuxcnc/tests/t0/random-with-t0/sim.ini:67:kinematics:trivkins",
"config:linuxcnc/tests/t0/random-without-t0/sim.ini:67:kinematics:trivkins",
"config:linuxcnc/tests/tlo/g43-test.ini:46:kinematics:trivkins",
"config:linuxcnc/tests/tool-info/non-random/test.ini:54:kinematics:trivkins",
"config:linuxcnc/tests/tool-info/random-no-startup-tool/test.ini:54:kinematics:trivkins",
"config:linuxcnc/tests/tool-info/random-with-startup-tool/test.ini:54:kinematics:trivkins",
"config:linuxcnc/tests/toolchanger/m61/m61-test.ini:39:kinematics:trivkins",
"config:linuxcnc/tests/toolchanger/m61/mod_core_sim.hal:5:loadrt:trivkins",
"config:linuxcnc/tests/toolchanger/reload-tool/non-random/test.ini:55:kinematics:trivkins",
"config:linuxcnc/tests/toolchanger/reload-tool/random/test.ini:55:kinematics:trivkins",
"config:linuxcnc/tests/toolchanger/toolno-pocket-differ/nonrandom/sim.ini:68:kinematics:trivkins",
"config:linuxcnc/tests/toolchanger/toolno-pocket-differ/random/sim.ini:68:kinematics:trivkins",
"config:linuxcnc/tests/trajectory-planner/circular-arcs/configs/core_sim_components.hal:5:loadrt:trivkins"
],
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"coordinates": {
"XYZ": "verified",
"XYZABCUVW": "verified",
"case_fold": "verified",
"duplicate_axes": "verified",
"extra_joint_count": "verified"
},
"extra_joints": {
"home_zeroes_kinematics_prefix": "verified",
"ignored_suffix": "verified",
"joint_count_mismatch": "verified",
"prefix_only": "verified"
},
"mapping_contract": "coverage/kinematics-mapping-contract.json",
"modes": {
"both": "verified",
"forward_only": "verified",
"identity": "verified",
"inverse_only": "verified"
},
"numeric_policy": "coverage/kinematics-numeric-policy.json",
"oracle": {
"configuration_candidates": [
"config:linuxcnc/configs/sim/axis/anglejog/anglejog.ini:54:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/axis.ini:144:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/axis_9axis.ini:67:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/axis_mm.ini:136:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/canterp.ini:47:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/classicladder/demo_sim_cl.ini:52:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini:10:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/db_demo/base.inc:38:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/external_offsets/dynamic_offsets.ini:46:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/external_offsets/eoffsets.ini:67:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/external_offsets/jwp_z.ini:52:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/external_offsets/opa.ini:56:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/extrajoints/12extrajoints.ini:19:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/extrajoints/1extrajoint.ini:22:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/extrajoints/2extrajoints.ini:25:kinematics:trivkins",
"config:linuxcnc/configs/sim/axis/foam/axis_foam.ini:64:kinematics:trivkins"
],
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "fec9dd7b8adc27ab05c2663320854b027d22cd5dcc7aab31c9597470b9724560",
"schema": "trivkins-integration-report.v1",
"source_ids": [
"source:linuxcnc/src/emc/kinematics/maxkins.c",
"source:linuxcnc/src/emc/kinematics/rotatekins.c",
"source:linuxcnc/src/emc/kinematics/trivkins.c"
],
"status": "partial",
"tests": {
"ctest": "sim_kinematics_test",
"native": "build-native/sim_kinematics_test",
"status": "passed"
}
}

View File

@@ -0,0 +1,153 @@
{
"boundaries": {
"missing_required_axis": "SIM_KIN_ERR_CONFIGURATION verified by existing contract tests",
"state_hash": "native/WASM identical",
"xyzac_round_trip": "verified",
"xyzbc_b_90_singular": "SIM_KIN_ERR_SINGULAR verified",
"xyzbc_round_trip": "verified"
},
"configuration_candidates": {
"xyzac": {
"joints": 5,
"kinematics": "xyzac-trt-kins",
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
"scan": true,
"sparm": "identityfirst"
},
"xyzbc": {
"joints": 5,
"kinematics": "xyzbc-trt-kins",
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
"scan": true,
"sparm": "identityfirst"
}
},
"implementation_status": "verified",
"native_wasm": {
"native_command": "build-native/sim_kinematics_test",
"status": "passed",
"wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"
},
"offsets": {
"xyzac": {
"tool_offset": 100.0,
"x_rot_point": 20.0,
"y_offset": 3.0
},
"xyzbc": {
"tool_offset": 50.0,
"x_offset": 5.0,
"z_offset": 2.0
}
},
"oracle": {
"reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"status": "blocked"
},
"report_sha256": "504f1e29c8c7e3d0d96c4113106401d0c44c1b82044461e43d1b2f5cf48fdfcc",
"rotation_chain": {
"direction": "LinuxCNC opposite conventional rotary directions retained",
"identityfirst": "switchkins type 0 identity, type 1 TRT, type 2 userk",
"xyzac": "C about Z then A about X with y/z/tool offsets",
"xyzbc": "C about Z then B about Y with x/z/tool offsets"
},
"samples": {
"byte_identical_json": true,
"native": {
"xyzac_trt": {
"inverse": [
100.0,
50.0,
200.0,
30.0,
45.0
],
"pose": [
142.826706376,
9.689621386,
163.102540378
],
"state_hash": 4869492769119734977
},
"xyzbc_trt": {
"inverse": [
100.0,
50.0,
200.0,
30.0,
45.0
],
"pose": [
44.740352549,
25.97032557,
227.67175976
],
"state_hash": 1734235358276017152
}
},
"wasm": {
"xyzac_trt": {
"inverse": [
100.0,
50.0,
200.0,
30.0,
45.0
],
"pose": [
142.826706376,
9.689621386,
163.102540378
],
"state_hash": 4869492769119734977
},
"xyzbc_trt": {
"inverse": [
100.0,
50.0,
200.0,
30.0,
45.0
],
"pose": [
44.740352549,
25.97032557,
227.67175976
],
"state_hash": 1734235358276017152
}
}
},
"schema": "trt-kinematics-report.v1",
"source_records": {
"implementation": {
"path": "wasm-core/src/sim_kinematics.c",
"sha256": "7a77915c59f190bcbe5c93d8f53498745bfcf978c167ba8878a21b914f9401fd"
},
"test": {
"path": "wasm-core/tests/sim_kinematics_test.c",
"sha256": "ceeb8b4100d034d76e70ce675d78b2d62c51e8e70be2205a9c8ae02e105a6aa6"
},
"trt_source": {
"path": "linuxcnc/src/emc/kinematics/trtfuncs.c",
"sha256": "ced2f9d4732681e2e82153b2f6343eccdf6972222d558d1e277bf1ec0bdc87fd"
},
"xyzac_config": {
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
"sha256": "9ee1d96962e7a44b25468f452543cfc3ca318585269e26227bb5b6e17b1e42f1"
},
"xyzac_source": {
"path": "linuxcnc/src/emc/kinematics/xyzac-trt-kins.c",
"sha256": "24c633613db9876c2ec246e1519a63d288fd27c68296d89f89f0368105634e69"
},
"xyzbc_config": {
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
"sha256": "78ce55d1e96e6f6a68f7352d530f9f84f3a68188cbe3b45e908a085295180c78"
},
"xyzbc_source": {
"path": "linuxcnc/src/emc/kinematics/xyzbc-trt-kins.c",
"sha256": "d79c06db82930ef685809a87c7207398f7893f56a6a0dfe382df2c81eaeb9562"
}
},
"status": "partial"
}

View File

@@ -5,17 +5,23 @@
**生成器**:[`scripts/context_packet.py`](../scripts/context_packet.py) **生成器**:[`scripts/context_packet.py`](../scripts/context_packet.py)
**测试**:[`scripts/test_context_packet.py`](../scripts/test_context_packet.py) **测试**:[`scripts/test_context_packet.py`](../scripts/test_context_packet.py)
本文件只解决两个问题:任务如何拆到可以持续接续的大小,以及一次任务交接究竟允许携带什么上下文。架构、功能范围、源文件关联和发布门禁分别以[技术路线](linuxcnc-web-wasm-technical-roadmap.md)、[源程序关联目录](linuxcnc-web-wasm-source-map.md)、[覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)和[执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)为准。本文件不复制这些文档的内容。 本文件只解决两个问题:任务如何拆到可以持续接续的大小,以及一次任务交接究竟允许携带什么上下文。架构、功能范围、源文件关联和发布门禁分别以[技术路线](linuxcnc-web-wasm-technical-roadmap.md)、[完整仿真机床实施蓝图](linuxcnc-web-wasm-full-port-implementation.md)、[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)、[源程序关联目录](linuxcnc-web-wasm-source-map.md)、[覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)和[执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)为准。本文件不复制这些文档的内容。
## 1. 不变量 ## 1. 不变量
1. 一个任务只有一个主工作流、一个里程碑、一个可验证结果和一个回滚点。跨三个以上工作流必须拆分。 1. 一个任务只有一个主工作流、一个里程碑、一个可验证结果和一个回滚点。跨三个以上工作流必须拆分。
2. 一个上下文包只有固定八段:`header`、`baseline`、`scope`、`contracts`、`evidence`、`risks`、`next`、`forbidden`。禁止追加 `history`、聊天记录或完整日志段。 2. 一个上下文包只有固定八段:`header`、`baseline`、`scope`、`contracts`、`evidence`、`risks`、`next`、`forbidden`。禁止追加 `history`、聊天记录或完整日志段。`nextTask.md` 只是 8 KiB 以内的启动指针,历史和证据摘要必须移到按需读取的索引。
3. 默认 profile 为 `feature`(4,096 tokens/16 KiB);里程碑任务才使用 `milestone`(8,192 tokens/32 KiB)。`doc` 和 `contract` 只在确有需要时使用。 3. 默认 profile 为 `feature`(4,096 tokens/16 KiB);里程碑任务才使用 `milestone`(8,192 tokens/32 KiB)。`doc` 和 `contract` 只在确有需要时使用。
4. `total_bytes` 同时限制序列化包和选中证据负载:`context_bytes = packet_bytes + selected_evidence_bytes`。`evidence_bytes` 是独立的证据总量上限,不能用文件数量替代。 4. `total_bytes` 同时限制序列化包和选中证据负载:`context_bytes = packet_bytes + selected_evidence_bytes`。`evidence_bytes` 是独立的证据总量上限,不能用文件数量替代。
5. 证据在生成阶段筛选。可选证据被排除时必须记录稳定 `reason_code`;必需证据无法容纳时生成失败,不输出部分成功包。 5. 证据在生成阶段筛选。可选证据被排除时必须记录稳定 `reason_code`;必需证据无法容纳时生成失败,不输出部分成功包。
6. 交接只携带最新包、context lock 摘要、受影响 manifest ID 和可重跑命令。完整日志、golden 和差异报告只通过路径、大小和哈希引用。 6. 交接只携带最新包、context lock 摘要、受影响 manifest ID 和可重跑命令。完整日志、golden 和差异报告只通过路径、大小和哈希引用。
7. 模型请求必须为输出预留独立预算。`context` 输入预算、请求外固定开销(system/developer、工具 schema 和必要的当前状态摘要)、`max_output_tokens` 和模型上下文窗口分别记录;不能把完整日志追加到输入后再把输出预算设为最大值。连续执行从最新包重建请求,旧对话只保留路径/哈希,不能累加。请求失败时先缩减输入证据或拆分任务,不自动重试同一超大请求。
8. 流式响应必须经过 [`scripts/model_stream.py`](../scripts/model_stream.py) 的有界 SSE 消费器。只有收到 `response.completed` 且 provider `usage` 通过校验时结果才可复用;断流、残缺 UTF-8、`response.failed`、`context_length_exceeded`、usage 缺失/不一致或 `max_output_tokens` 截断都必须写入 metadata-only checkpoint。checkpoint 只能保存 `context_id`、`packet_id`、事件/字节计数、输出哈希和恢复建议,不得保存半截模型文本;恢复必须生成新的、更小的 packet。
9. `context_packet.py` 的 `token_limit`、`total_bytes`、`evidence_bytes` 和 `max_files` 覆盖值只能小于或等于 profile 默认值;扩大值必须改用新的、经过审查的 profile。没有 provider tokenizer 时,生成器和校验器按 UTF-8 字节作硬上界;调用方拿到精确令牌数后可通过 `--input-tokens` 替换保守估算,但不得放宽 profile 上限。
### 1.1 包的分层读取 ### 1.1 包的分层读取
任务执行者按以下顺序读取,读到足够完成任务即停止: 任务执行者按以下顺序读取,读到足够完成任务即停止:
@@ -137,6 +143,29 @@ next: INV-06b
| COM-05a | 能力/限制注册表 | 全量 manifest | INV-02b、CORE-07b | contract | | COM-05a | 能力/限制注册表 | 全量 manifest | INV-02b、CORE-07b | contract |
| COM-05b | 未支持能力解释 | 能力注册表 | COM-05a | doc | | COM-05b | 未支持能力解释 | 能力注册表 | COM-05a | doc |
#### 3.3.1 K0-K8:运动学主线
运动学任务按实现族和验收闭环拆分,完整契约、来源文件和测试矩阵见[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)。下列 ID 是最小任务目录的入口,不得把 CORE-07 作为一个不可拆分的大任务:
| ID | 单一结果 | 依赖 | Profile |
|---|---|---|---|
| K0-INV-01 | 运动学源文件、符号、缺失测试输入审计 | INV-01..02 | milestone |
| K0-INV-02 | `configs/sim` 运动学入口图和参数样本 | K0-INV-01 | feature |
| K0-ORACLE-01 | native FK/IK JSONL golden schema 和样本 | K0-INV-01 | contract |
| K1-CONTRACT-01 | pose/joint、flags、home、错误码 C ABI | K0-ORACLE-01、CORE-02 | contract |
| K1-CONTRACT-02 | 坐标映射、extra joints、数值/迭代政策 | K1-CONTRACT-01 | feature |
| K2-TRIV-01 | trivkins 四种类型、重复轴和 home | K1-CONTRACT-02 | feature |
| K2-COUPLED-01 | corexy/rose/max/rotate 及 HAL 等价链 | K2-TRIV-01、RT-02a | feature |
| K3-5AXIS-01 | 5axiskins、xyzac/xyzbc TRT | K2-COUPLED-01 | feature |
| K3-ROBOT-01 | GenSer、SCARA、PUMA、Scorbot | K1-CONTRACT-02 | feature |
| K4-PARALLEL-01 | Delta、RotaryDelta、Tripod | K1-CONTRACT-02 | feature |
| K4-PARALLEL-02 | Hexapod、Pentapod 迭代正解和分支 | K4-PARALLEL-01 | feature |
| K5-SWITCH-01 | switchkins 静态切换和 lastpose | K3、K4 | contract |
| K5-USER-01 | userk/ugenser 构建期静态能力 | K5-SWITCH-01 | feature |
| K6-MOTION-01 | TP→IK→motion→FK feedback | K2..K5、CORE-04/06 | milestone |
| K7-REG-01 | 全配置、UI 姿态和浏览器故障回归 | K6、FE/COM | milestone |
| K8-REL-01 | 运动学兼容、来源和发布闭合 | K7、REL-01 | milestone |
### 3.4 M4-M5:前端、回归和发布 ### 3.4 M4-M5:前端、回归和发布
| ID | 单一结果 | 直接输入 | 依赖 | Profile | | ID | 单一结果 | 直接输入 | 依赖 | Profile |
@@ -176,7 +205,7 @@ next: INV-06b
### 4.1 正常交接 ### 4.1 正常交接
1. 从 context lock 和父任务摘要生成新的 `packet_id`;只填本子任务的八段。 1. 从 context lock 和父任务摘要生成新的 `packet_id`;只填本子任务的八段。
2. 用候选证据的实际字节数调用生成器。生成前就排除 `E1`-`E5`,输出后只接受 `budget.packet_bytes <= byte_limit` 且 `packet_bytes + selected_evidence_bytes <= byte_limit`。 2. 用候选证据的实际字节数调用生成器。生成前就排除 `E1`-`E6`,输出后只接受 `budget.packet_bytes <= byte_limit`、`packet_bytes + selected_evidence_bytes <= byte_limit` 且估算输入令牌不超过 profile 上限。
3. 完成一步就更新 `evidence`、`risks`、`next`;不追加旧包全文。 3. 完成一步就更新 `evidence`、`risks`、`next`;不追加旧包全文。
4. 提交正文只写 `Context-ID`、`Context-Packet`、子任务 ID、测试命令和证据路径。 4. 提交正文只写 `Context-ID`、`Context-Packet`、子任务 ID、测试命令和证据路径。
@@ -201,7 +230,7 @@ budget.packet_bytes <= byte_limit
budget.packet_bytes + budget.selected_evidence_bytes <= byte_limit budget.packet_bytes + budget.selected_evidence_bytes <= byte_limit
selected_evidence_bytes <= evidence_byte_limit selected_evidence_bytes <= evidence_byte_limit
selected_evidence_files <= evidence_file_limit selected_evidence_files <= evidence_file_limit
所有排除项都有 E1-E5 reason_code 所有排除项都有 E1-E6 reason_code
required 证据无法容纳时命令失败且不写输出 required 证据无法容纳时命令失败且不写输出
``` ```
@@ -209,6 +238,10 @@ required 证据无法容纳时命令失败且不写输出
```bash ```bash
python3 -m unittest discover -s scripts -p 'test_*.py' -v python3 -m unittest discover -s scripts -p 'test_*.py' -v
python3 scripts/validate_task_pointer.py
python3 scripts/validate_model_budget.py <packet.json> --context-window <model-window> --overhead-tokens <current-request-overhead>
``` ```
流式请求还必须通过 `scripts/test_model_stream.py`:覆盖跨 chunk UTF-8、无终态断流、provider failure/context overflow、usage 校验、输出截断和 checkpoint 不包含部分文本。网络传输异常只能记录为 `stream_disconnected`,不能重试原 packet。
本规范的目标不是把所有信息压进一个更大的包,而是让每个任务只携带完成当前结果所需的最小、可重建、可审计上下文。 本规范的目标不是把所有信息压进一个更大的包,而是让每个任务只携带完成当前结果所需的最小、可重建、可审计上下文。

View File

@@ -3,7 +3,7 @@
**文档状态**:Draft v0.1 **文档状态**:Draft v0.1
**适用范围**:本仓库的纯 Web/WASM 仿真移植、浏览器运行时、OPFS 工程存储、覆盖矩阵和发布物 **适用范围**:本仓库的纯 Web/WASM 仿真移植、浏览器运行时、OPFS 工程存储、覆盖矩阵和发布物
**固定基线**:LinuxCNC `2.10.0~pre0`(以 `linuxcnc/VERSION` 和锁定的源码提交为准) **固定基线**:LinuxCNC `2.10.0~pre0`(以 `linuxcnc/VERSION` 和锁定的源码提交为准)
**关联文档**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)、[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)、[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)、[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)、[总控执行手册](linuxcnc-web-wasm-integration-playbook.md)、[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md) **关联文档**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)、[完整仿真机床实施蓝图](linuxcnc-web-wasm-full-port-implementation.md)、[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)、[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)、[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)、[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)、[总控执行手册](linuxcnc-web-wasm-integration-playbook.md)、[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md)
本文件是项目执行的治理入口。技术路线回答“做什么以及采用什么架构”,覆盖矩阵回答“逐项覆盖什么以及如何验收”,本文件回答“谁在什么固定上下文中交付、如何在断网或失败后接续、什么证据才能过门禁”。三份文档必须以同一基线版本更新;若规则冲突,以本文件的交付门禁和覆盖矩阵的逐项验收要求为准。 本文件是项目执行的治理入口。技术路线回答“做什么以及采用什么架构”,覆盖矩阵回答“逐项覆盖什么以及如何验收”,本文件回答“谁在什么固定上下文中交付、如何在断网或失败后接续、什么证据才能过门禁”。三份文档必须以同一基线版本更新;若规则冲突,以本文件的交付门禁和覆盖矩阵的逐项验收要求为准。
@@ -76,6 +76,8 @@ M0 必须冻结来源扫描和规范化规则;每个来源项至少映射到
“固定上下文”指每次任务交接和每次提交都重新装载一个固定格式、固定预算的上下文包,而不是把整个历史对话、全部日志或已完成阶段重新带入。上下文包是可丢弃的工作索引;完整源码、日志、golden 和差异报告留在仓库或 CI 产物中,只通过路径和摘要引用。 “固定上下文”指每次任务交接和每次提交都重新装载一个固定格式、固定预算的上下文包,而不是把整个历史对话、全部日志或已完成阶段重新带入。上下文包是可丢弃的工作索引;完整源码、日志、golden 和差异报告留在仓库或 CI 产物中,只通过路径和摘要引用。
启动顺序同样受预算约束:先只读取 `nextTask.md` 指针(不超过 8 KiB),再按当前子任务读取 `context-lock.json` 摘要和直接输入。`docs/task-history.md`、完整路线图、覆盖矩阵、`coverage/*.jsonl` 和完整 stdout/stderr 均为按需 L3 资料,不能在启动时批量加载。
上下文包使用固定的八段结构,逻辑预算为 **8,192 tokens,UTF-8 序列化不超过 32 KiB**。实现上由 [`scripts/context_packet.py`](../scripts/context_packet.py) 在选择证据时执行预算;校验器不能成为第一次发现超限的地方。每段有上限,缺省内容用 `none`,不得追加自由历史段: 上下文包使用固定的八段结构,逻辑预算为 **8,192 tokens,UTF-8 序列化不超过 32 KiB**。实现上由 [`scripts/context_packet.py`](../scripts/context_packet.py) 在选择证据时执行预算;校验器不能成为第一次发现超限的地方。每段有上限,缺省内容用 `none`,不得追加自由历史段:
| 段 | 固定内容 | 建议上限 | | 段 | 固定内容 | 建议上限 |
@@ -91,6 +93,19 @@ M0 必须冻结来源扫描和规范化规则;每个来源项至少映射到
包的 `packet_id` 由 `context_id`、任务 ID 和修订号组成;修订只替换对应段,不复制旧包。超过任一段上限或总预算时,必须先拆分任务、生成摘要或把内容移到可寻址产物,不能扩大上下文预算。每次交接从最新包重建,旧包只保留哈希和归档链接,因此移植从阶段 A 深入阶段 E 时,上下文大小不变。 包的 `packet_id` 由 `context_id`、任务 ID 和修订号组成;修订只替换对应段,不复制旧包。超过任一段上限或总预算时,必须先拆分任务、生成摘要或把内容移到可寻址产物,不能扩大上下文预算。每次交接从最新包重建,旧包只保留哈希和归档链接,因此移植从阶段 A 深入阶段 E 时,上下文大小不变。
模型调用还必须显式保留输出空间:
```text
request_input_tokens = packet_input_tokens + fixed_overhead_tokens
model_context_window >= request_input_tokens + max_output_tokens + safety_margin
```
请求前由 `validate_model_budget.py` 阻断超限;请求后还必须由 `validate_provider_response` 校验 provider 返回的 usage,确认 `output_tokens <= max_output_tokens`。若 `finish_reason` 为 `length`/`max_tokens`/`max_output_tokens`,只保存 metadata-only checkpoint 并从最新 packet 发起更小的新请求,不能把截断响应追加回下一轮。
`fixed_overhead_tokens` 必须由调用方按当前请求重新计数,不能把上一次请求的历史累加进去。`max_output_tokens` 应按任务类型固定(文档 768、feature 1,024、contract 1,536、milestone 2,048 的默认上限),而不是使用模型的最大值;调用参数只能缩小 profile 的输入/输出预算,不能放大。`context_packet.py` 的字节、证据文件数和令牌覆盖值同样只能缩小 profile。没有 provider tokenizer 时按 UTF-8 字节作硬上界,拿到精确令牌数后可用 `--input-tokens` 替换估算。输入包达到预算 80% 或模型返回 `context_length_exceeded` 时,先移除最低优先级证据、缩短摘要或拆分子任务;不得把旧对话和失败响应再次拼回请求。
流式调用必须由 [`scripts/model_stream.py`](../scripts/model_stream.py) 的有界 SSE 解码器消费。只有 `response.completed` 且 `usage.input_tokens/output_tokens/total_tokens` 一致、输出未超 `max_output_tokens` 时才标记可复用;中途断流、解码失败、`response.failed`、上下文超限或 `response.incomplete` 只生成 metadata-only checkpoint。恢复动作固定为丢弃可选证据并从新 packet 重建,禁止续接半截文本或重试同一个超大请求。
#### 生成阶段的预算算法 #### 生成阶段的预算算法
证据选择必须在写入输出文件前完成,按以下顺序执行: 证据选择必须在写入输出文件前完成,按以下顺序执行:
@@ -99,9 +114,9 @@ M0 必须冻结来源扫描和规范化规则;每个来源项至少映射到
2. 将候选证据规范化为 `path`、`bytes`、`sha256`、`kind`、`priority`、`required`,按必需、优先级、路径排序,保证选择确定性。 2. 将候选证据规范化为 `path`、`bytes`、`sha256`、`kind`、`priority`、`required`,按必需、优先级、路径排序,保证选择确定性。
3. 逐项检查:单项证据字节预算、证据总字节预算、证据文件数,以及把该项加入 `selected` 后的完整上下文剩余空间;完整上下文按 `packet_bytes + selected_evidence_bytes` 计算,不能只计算 JSON 清单。 3. 逐项检查:单项证据字节预算、证据总字节预算、证据文件数,以及把该项加入 `selected` 后的完整上下文剩余空间;完整上下文按 `packet_bytes + selected_evidence_bytes` 计算,不能只计算 JSON 清单。
4. 可选项无法容纳时不写入包,记录稳定排除码;必需项无法容纳时直接失败,不生成部分成功包。 4. 可选项无法容纳时不写入包,记录稳定排除码;必需项无法容纳时直接失败,不生成部分成功包。
5. 生成最终预算元数据后再次序列化校验;若审计清单本身使包超限,从最低优先级可选项回退并以 `E5=context_remaining_bytes_insufficient` 记录。若只剩必需项仍超限,失败。 5. 生成最终预算元数据后再次序列化校验;若审计清单或令牌估算使包超限,从最低优先级可选项回退并以 `E5=context_remaining_bytes_insufficient` 或 `E6=token_budget_exceeded` 记录。若只剩必需项仍超限,失败。
排除原因使用稳定机器码,避免长文本侵占上下文:`E1 duplicate_path`、`E2 evidence_item_bytes_exceed_budget`、`E3 evidence_file_count_exceed_budget`、`E4 evidence_bytes_exceed_budget`、`E5 context_remaining_bytes_insufficient`。输出必须包含选中证据字节数、选中/排除文件数、证据预算、总包预算、最终包字节数和排除码;CI 同时检查 `packet_bytes <= byte_limit` 和 `packet_bytes + selected_evidence_bytes <= byte_limit`,而不是先写超限包再报告错误。 排除原因使用稳定机器码,避免长文本侵占上下文:`E1 duplicate_path`、`E2 evidence_item_bytes_exceed_budget`、`E3 evidence_file_count_exceed_budget`、`E4 evidence_bytes_exceed_budget`、`E5 context_remaining_bytes_insufficient`、`E6 token_budget_exceeded`。输出必须包含选中证据字节数、选中/排除文件数、证据预算、总包预算、最终包字节数和排除码;CI 同时检查 `packet_bytes <= byte_limit`、`packet_bytes + selected_evidence_bytes <= byte_limit` 和 token 上限,而不是先写超限包再报告错误。
上下文包的最小机器可读形状如下;CI 应检查字段齐全、段顺序、预算、`context_id` 一致性和引用产物存在: 上下文包的最小机器可读形状如下;CI 应检查字段齐全、段顺序、预算、`context_id` 一致性和引用产物存在:
@@ -262,8 +277,8 @@ online_ready
|---|---|---|---| |---|---|---|---|
| M0 基线锁定 | A | context lock、来源/测试清单、状态枚举、责任分工 | 清单可重建;缺项 CI 失败;文档链接和基线一致 | | M0 基线锁定 | A | context lock、来源/测试清单、状态枚举、责任分工 | 清单可重建;缺项 CI 失败;文档链接和基线一致 |
| M1 运行时骨架 | A | 单 Worker、固定 tick、ABI、最小 OPFS journal、Python 启动 | create/load/step/snapshot/recover ABI 测试通过;无网加载缓存样例 | | M1 运行时骨架 | A | 单 Worker、固定 tick、ABI、最小 OPFS journal、Python 启动 | create/load/step/snapshot/recover ABI 测试通过;无网加载缓存样例 |
| M2 核心行为 | B | G/M/O、TP、坐标、补偿、kinematics、原生 golden | 受影响核心 manifest 全部 `verified`;首个 divergent tick 可定位 | | M2 核心行为 | B | G/M/O、TP、坐标、补偿、[K0-K5 运动学主线](linuxcnc-web-wasm-kinematics-migration-plan.md)、原生 golden | 受影响核心 manifest 全部 `verified`;每个运动学族的首个 divergent tick 可定位 |
| M3 虚拟设备与持久化 | C | HAL/PLC/IO、数字孪生、事务恢复、断网接续 | 故障注入和恢复矩阵通过;配额不足不损坏已提交版本 | | M3 虚拟设备与持久化 | C | HAL/PLC/IO、K6 运动集成、数字孪生、事务恢复、断网接续 | 故障注入和恢复矩阵通过;配额不足不损坏已提交版本 |
| M4 Web 工作流 | D | 控制台、3D、VCP/配置工具、导入导出、离线 PWA | 关键 UI 工作流在浏览器矩阵通过;键盘/触摸和刷新恢复有证据 | | M4 Web 工作流 | D | 控制台、3D、VCP/配置工具、导入导出、离线 PWA | 关键 UI 工作流在浏览器矩阵通过;键盘/触摸和刷新恢复有证据 |
| M5 发布候选 | E | 全量覆盖、兼容/许可证报告、迁移工具、离线包 | 所有可计入条目 `accepted`,排除项可识别,发布门禁全绿 | | M5 发布候选 | E | 全量覆盖、兼容/许可证报告、迁移工具、离线包 | 所有可计入条目 `accepted`,排除项可识别,发布门禁全绿 |
@@ -273,7 +288,7 @@ online_ready
### 7.1 合并门禁 ### 7.1 合并门禁
每个变更包至少通过:文档链接和术语检查、格式/静态检查、受影响单测、manifest 完整性检查、原生/WASM 差异测试(适用时)、离线影响场景(适用时)、`context_packet.py` 生成器测试、上下文包字段/大小检查和证据包字段检查。任何新增来源、配置、组件、G/M/O 功能或 UI 工作流没有 manifest 条目时 CI 失败。上下文包超过 8,192 tokens 或 32 KiB、出现未定义段、把旧包全文嵌入新包,均直接阻止合并。生成阶段不能产生超限包;验证阶段只能复核,不能负责裁剪。 每个变更包至少通过:文档链接和术语检查、格式/静态检查、受影响单测、manifest 完整性检查、原生/WASM 差异测试(适用时)、离线影响场景(适用时)、`context_packet.py` 生成器测试、`validate_task_pointer.py` 启动指针检查、`validate_model_budget.py` 请求预算检查、上下文包字段/大小检查和证据包字段检查。任何新增来源、配置、组件、G/M/O 功能或 UI 工作流没有 manifest 条目时 CI 失败。上下文包超过 8,192 tokens 或 32 KiB、启动指针超过 8 KiB、模型输入加输出预留超出窗口、出现未定义段、把旧包全文嵌入新包,均直接阻止合并。生成阶段不能产生超限包;验证阶段只能复核,不能负责裁剪。
### 7.2 里程碑门禁 ### 7.2 里程碑门禁

View File

@@ -0,0 +1,258 @@
# LinuxCNC Web/WASM 完整仿真机床实施蓝图
**文档状态**:实施规范 v1.0
**适用基线**:LinuxCNC `2.10.0~pre0`,提交由 [`context-lock.json`](../context-lock.json) 冻结
**用途**:把“完整移植 LinuxCNC 仿真机床”分解为可实现、可回归、可恢复和可发布的工程闭环
**运动学主线**:[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)
**上位路线**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)
**覆盖依据**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)
**源程序依据**:[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)
**通信契约**:[前端与通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)
**执行总控**:[仿真移植总控执行手册](linuxcnc-web-wasm-integration-playbook.md)
本文件是实施层的补充规范。运动学实现族、正逆解契约和 K0-K8 里程碑以[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)为专项细则。它不替代来源清单、协议 schema 或覆盖矩阵;当这些文件发生冲突时,`context-lock.json`、协议 schema 和逐文件覆盖记录优先。本文件解决的是:一个 LinuxCNC 仿真入口如何启动,经过哪些运行时层,如何产生确定性结果,如何在浏览器中恢复,以及怎样证明“完整”而不是“演示可用”。
## 1. 完整移植的工程定义
完整移植必须同时闭合六条链。任何一条链缺失,都只能称为部分移植或演示版本:
| 闭合链 | 必须回答的问题 | 发布前证据 |
|---|---|---|
| 来源闭合 | LinuxCNC 文档、源码、符号、配置资源和测试是否全部登记? | `source-index`、`manifest`、依赖闭包、零孤儿报告 |
| 构建闭合 | 可移植代码、适配层、静态组件和 Python 运行时是否可重建? | 编译锁、构建日志、产物 SHA-256、许可证清单 |
| 启动闭合 | 任一纳入范围的 `configs/sim` 入口能否解析并进入 `READY` 或给出可诊断错误? | 配置资源图、启动日志、错误文件/行号 |
| 行为闭合 | 解释器、TP、运动、HAL、IO、PLC 和 Python 的可观察结果是否与原生 oracle 一致? | JSONL golden、首个 divergent tick 报告、容差说明 |
| 工作流闭合 | 编辑、MDI、运行、暂停、单步、急停、jog、home、工具、报警和恢复是否能从 UI 完成? | 浏览器 E2E、键盘/触摸、故障和恢复场景 |
| 交付闭合 | 离线、迁移、配额、崩溃恢复、回滚、兼容性和许可证是否可审计? | 发布清单、离线包、迁移/回滚演练、兼容报告 |
“全覆盖”只统计仿真可观察能力。真实硬件、硬实时内核和安全认证属于 `REAL-EXCLUDED`;但它们在仿真配置中产生的信号必须由数字孪生提供,不能以“不支持”跳过主轴、编码器、探针、限位、换刀、冷却或 IO 的仿真行为。
## 2. 当前起点和硬门禁
当前仓库的实际起点如下,不能用文档中的目标状态替代代码证据:
| 项目 | 当前状态 | 实施含义 |
|---|---|---|
| M0 来源/上下文 | 已生成,锁定 8,026 个来源条目和 context lock | 任何来源变化先重建并审查哈希 |
| M1 ABI 骨架 | 已有生命周期、固定 tick、快照和导入导出骨架 | 只能作为 ABI 起点,不代表 LinuxCNC 核心已接入 |
| posemath/解释器/运动切片 | 已有仓库内适配报告和原生测试证据 | 仍需接入统一 `sim_task`、HAL 和 Worker |
| kinematics 节点 | 旧 `M2-CORE-04` blocked,当前转入 K0 来源审计 | 锁定来源缺少声明的 `linuxcnc/unit_tests/kinematics/**`,不得静默改用其他目录 |
| Python、完整 HAL、UI、OPFS 恢复 | 尚未完成全量闭环 | 不得以 mock、刷新后重建或单一三轴页面宣称完成 |
每个实施任务开始前运行基线门禁:
```bash
python3 scripts/validate_baseline.py
python3 scripts/validate_download_index.py
python3 -m unittest discover -s scripts -p 'test_*.py' -v
python3 scripts/validate_task_pointer.py
```
若锁定提交、输入目录、schema 版本或生成器版本发生变化,先创建新的 `context_id`;不能在旧 context 中覆盖索引或降低校验条件。
## 3. 运行时分层和依赖规则
完整仿真运行时固定为单 Dedicated Worker 单写者模型。主线程只能通过版本化协议读取状态和提交命令,不能读取 WASM 指针、修改 HAL pin 或直接写 OPFS。
```text
UI / Three.js / 编辑器
│ protocol.v1
Worker: CommandRouter → TaskLedger → SimScheduler
│ ├─ SimTask / rs274ngc / canonical
│ ├─ TP / posemath / kinematics
│ ├─ SimMotion / homing / limits
│ ├─ SimHAL / components / ClassicLadder
│ ├─ SimIO / digital-twin devices
│ └─ PythonRuntime / remap / VCP
├─ SnapshotPublisher / EventBus / TraceRecorder
└─ WorkspaceRuntime → WasmFS/POSIX → OPFS journal/checkpoint
```
依赖必须单向流动:
1. 几何、解释器和运动核心不能依赖 DOM、OPFS、Python 或 UI。
2. HAL 组件只能通过注册的 pin/signal/parameter 接口访问运行时;组件不得调用宿主路径、网络或动态加载。
3. Python 只能访问白名单虚拟模块、`/workspace` 和虚拟时钟;超出能力返回稳定错误码。
4. UI 只能消费快照、事件和流;派生显示状态不得回写 authoritative state。
5. OPFS 只承接事务和恢复,不参与每个仿真 tick 的运动计算。
WASM 核心以独立 CMake/Emscripten 目标构建。LinuxCNC 原生 `meson.build` 继续用于 oracle 和原生测试,不作为 WASM 的直接构建入口。动态库、`fork/exec`、NML/CMS 进程边界和共享内存改为静态注册、进程内对象和版本化快照。
## 4. 一个仿真入口的启动闭环
所有 `configs/sim/**` 入口都必须经过相同的启动状态机;配置专属差异只能来自已登记能力和资源,不得在 UI 中写隐式特例。
```text
NEW
→ WORKSPACE_MOUNTED
→ INI_PARSED
→ RESOURCES_RESOLVED
→ CORE_CREATED
→ HAL_TWOPASS_DONE
→ DEVICES_BOUND
→ TASK_READY
→ RUNNING / PAUSED / ESTOP
→ STOPPED → DESTROYED
```
启动步骤及其产物:
1. `HANDSHAKE`:协商 `context_id`、ABI/protocol/snapshot 版本和能力注册表;版本不兼容直接拒绝。
2. `MOUNT`:将工程挂载到虚拟 `/workspace`,规范化大小写、路径和编码,拒绝 `..`、绝对宿主路径和符号链接。
3. `PARSE_INI`:解析 section、include、变量替换、轴/关节/主轴数量、周期和入口文件;错误必须包含文件、行和字段。
4. `RESOLVE`:构建 `.hal`、`.tbl`、`.var`、`.ngc`、`.py`、`.xml/.glade/.ui`、`.clp`、`.halscope/.halshow` 等资源图,报告缺失、循环和宿主依赖。
5. `CREATE_CORE`:按配置静态选择 kinematics、解释器、TP、motion、IO 和工具服务,记录模块版本与 source SHA。
6. `HAL_TWOPASS`:完成 `loadrt`、组件实例、`addf`、`setp`、`net`、`sets`、`link*` 的确定性顺序;任何未注册组件阻断启动。
7. `BIND_DEVICES`:绑定轴、编码器、主轴、探针、限位、回零、换刀、冷却和 PLC 数字孪生,并应用故障模型。
8. `START_TASK`:加载程序、参数、工具表和 Python/remap;运行一次零周期初始化,生成初始状态哈希。
9. `READY`:发布完整快照、能力和资源诊断。UI 只有收到 `READY` 才能开放运行按钮。
任一步骤失败时,必须撤销尚未提交的资源绑定并保留结构化诊断;不能留下半初始化会话,也不能通过“忽略失败文件”进入 `READY`。
## 5. 固定 tick 和状态更新顺序
`sim_step(cycles)` 是唯一改变仿真结果的入口。每个逻辑周期按下列顺序执行,顺序属于协议和 golden 的一部分:
1. 读取本周期已确认的命令,按 `seq` 和 `operation_id` 去重;未确认的命令不能重复产生副作用。
2. 执行 Task/解释器预算,产生 canonical 队列、当前行、模态和错误。
3. 运行 TP 前瞻、blend、速度/加速度/jerk 限制,生成本周期目标轨迹。
4. 调用 kinematics `inverse`,检查奇异姿态、轴/关节限制和 NaN/Inf。
5. 更新 motion、homing、软/硬限位、following error、急停和 jog 状态。
6. 按静态线程清单运行 HAL function、虚拟轴/编码器/主轴/探针/换刀和 ClassicLadder scan。
7. 处理 IO 完成、输入等待、超时、报警和 Python 回调;回调不能绕过当前 tick。
8. 生成事件、采样和状态哈希;先发布内存快照,再按 checkpoint/journal 策略持久化。
暂停只停止后续运动和解释,不丢弃已入队 canonical;急停清空或冻结哪些队列必须由状态机明确规定。快速模式、1x、单步、暂停恢复和回放在相同逻辑终点必须得到相同状态哈希;墙钟只用于节流和让出 Worker 事件循环。
## 6. 各域实施契约
### 6.1 配置、工作区和启动资源
实现顺序为“路径规则 → 文件 API → 解析器 → 资源图 → 启动器”。至少提供 `open/read/write/stat/readdir/rename/unlink/fsync`,并保留 LinuxCNC 相对路径和 include 语义。
| 输入类型 | 处理方式 | 最小验收 |
|---|---|---|
| `.ini/.inc` | 兼容 section、变量、include、相对路径和循环检测 | 逐文件行号诊断、重复加载结果一致 |
| `.hal/.comp` | 解析 twopass、实例、函数顺序和 pin 类型;组件构建期静态注册 | HAL graph 与 native 对齐 |
| `.tbl/.var` | 工具、pocket、offset、参数和 wear 的版本化服务 | 加载、编辑、提交、恢复一致 |
| `.ngc/.nc` | 主程序、子程序、O-word、M-code 文件和过滤器 | 相同入口解析到相同程序图 |
| `.py` | 受控 Python runtime;能力清单和资源限额 | 正常、异常、超时、禁止 API |
| `.xml/.glade/.ui` | 转换为 Web Component descriptor;保留 pin、属性、回调 | 样例面板可加载且权限可审计 |
| `.clp/.halscope/.halshow` | 解析 PLC、采样、触发和回放描述 | 逻辑/波形结果可回放 |
资源图必须记录 `input_sha`、解析器版本、依赖、输出能力和错误。配置加载不成功时,错误报告必须能反向定位到 manifest 条目。
### 6.2 解释器、Task、TP、motion 和 kinematics
移植以原生 oracle 为先:先让 native runner 输出解释器状态、canonical、每 tick 运动和错误,再接入 WASM。核心边界只允许使用固定宽度类型和稳定错误码。
实施顺序:
1. posemath 和数值策略:固定浮点编译选项、角度/单位换算、NaN/Inf 检查。
2. rs274ngc:词法/语法、模态、参数、O-code、循环、补偿、G/M/O 和错误位置。
3. canonical/TP:直线、圆弧、球面弧、样条、NURBS、前瞻、blend、exact stop 和同步运动。
4. Task/NML 兼容:命令、状态、错误、模式转换、MDI、单步、暂停、恢复、abort 和队列清空。
5. motion:轴/关节、jog、home、软硬限位、following error、外部偏置和故障注入。
6. kinematics:trivkins、gantry、CoreXY、5/6 轴、Delta、机器人、foam 等静态模块;每个模块提供 `forward/inverse/limits/capabilities`。
`M2-CORE-04` 当前因锁定输入缺少 `unit_tests/kinematics/**` 而 blocked。处理方式只有两种:补充并重新锁定确实存在的官方测试输入,或由契约变更明确批准替代 oracle 并登记差异;不能把 `tests/matrixkins/**` 直接伪装成原声明输入。
### 6.3 HAL、组件、PLC 和数字孪生
HAL 运行时必须先建立类型化对象图,再执行组件。组件来源按三类登记:可移植核心、构建期 `.comp` 组件、仿真专用数字孪生。每个组件 manifest 至少包含名称、版本、输入/输出 pin、参数、线程要求、依赖、权限、测试和状态。
固定调度顺序为:`base/servo`(如配置启用)→运动反馈→主轴/编码器→通用 HAL function→PLC scan→IO 完成→采样发布。缺少实时墙钟不改变这个逻辑顺序。
数字孪生必须能脚本化以下状态:主轴加减速/不到速/定向失败,编码器计数和丢脉冲,探针触发/未触发,限位/回零,换刀成功/超时/失败,冷却和润滑,模拟/数字 IO 延迟、抖动和故障。故障注入本身也是可回放输入,不能通过随机数产生不可重复结果。
ClassicLadder 需要覆盖工程加载、寄存器、触点/线圈、定时器、计数器、顺序控制、HAL 绑定、扫描周期、暂停/恢复和诊断 UI;真实 Modbus transport 排除,但虚拟寄存器、延迟和错误状态必须可配置。
### 6.4 Python、remap、VCP 和插件
Python runtime 在 Worker 内启动,固定 Pyodide 版本、包索引和 artifact hash。桥接模块只提供 `linuxcnc`、`hal`、`gstat`、INI、工具表、虚拟时间和工作区 API;`subprocess`、`socket`、宿主线程、设备文件和任意路径必须返回稳定的能力拒绝错误。
remap/filter/VCP 每次调用都记录入口、参数摘要、tick、返回值、异常、耗时和资源消耗。Python 异常必须回到解释器/Task 错误语义,不能只写 console。VCP descriptor 转换为带版本和权限的 Web Component schema,插件崩溃不得破坏核心会话。
### 6.5 协议、快照、事件和持久化
命令必须执行 `prepare → execute → verify → commit`;`operation_id` 幂等,回答丢失时先查账本。快照至少分为配置、解释器、Task、TP、motion、kinematics、HAL、IO、Python、工作区和恢复元数据,并带 `schema_version/context_id/simulation_tick/state_hash`。
OPFS 只保存已提交工程、journal、checkpoint、日志和可删除缓存。推荐恢复流程:读取最近有效 checkpoint → 校验哈希 → 按序重放已提交 journal → 对未确认操作逐项对账 → 发布 `RECOVERED` 或明确 `RECOVERY_BLOCKED`。不能以“重新加载 INI 并从头运行”替代状态恢复。
## 7. 原生 oracle 和全量回归
原生 runner 与 WASM runner 必须使用同一工程输入和同一命令序列。每个样本至少输出:
```json
{
"context_id": "...",
"entry_source_id": "...",
"test_id": "...",
"tick": 1234,
"interpreter": {"line": 12, "modal_hash": "...", "parameters_hash": "..."},
"canonical": [{"kind": "straight_feed", "target": [0, 0, 0]}],
"motion": {"axis": [], "joint": [], "following_error": []},
"hal": {"pins_hash": "...", "signals_hash": "..."},
"io": {"spindle": {}, "tool": {}, "probe": {}},
"events": [],
"state_hash": "..."
}
```
差异报告必须给出首个 divergent tick、字段路径、原生值、WASM 值、单位、容差、来源文件和 manifest ID。浮点容差只能用于数值字段,不能掩盖状态转换、错误码、事件顺序、当前行、工具号或 HAL 拓扑差异。
每个入口至少执行以下维度:正常运行、边界数值、语法/配置错误、暂停/单步/abort、急停/限位/设备故障、导出导入、Worker 重启/OPFS 恢复、离线启动;有 UI 或插件的入口再加键盘/触摸、权限和背压维度。
发布前必须满足:
- `source-index`、`manifest`、依赖闭包和配置资源图无孤儿,所有条目已分配 owner/feature/domain。
- 所有可计入条目为 `accepted`;不得残留 `planned`、`needs_owner`、无 oracle 或无测试维度条目。
- 同一输入在至少三次运行、1x/快速/单步/恢复路径下的最终 `state_hash` 一致。
- 所有事件按 `seq/tick` 单调发布,重放不重复产生已提交副作用。
- 配额不足、schema 不兼容、缺失资源、禁止宿主能力和恢复无法对账均有明确错误,不静默降级。
- 浏览器矩阵、性能、内存、离线、许可证、迁移和回滚报告齐全;性能优化不能改变 golden 结果。
## 8. 分阶段实施和产物
| 阶段 | 实施重点 | 必须产物 | 退出条件 |
|---|---|---|---|
| M0 基线闭合 | 来源、符号、配置、测试、依赖和 context lock | 索引、manifest、oracle schema、影响图 | 零孤儿、哈希可重建、缺字段会失败 |
| M1 会话闭环 | C ABI、Worker 队列、虚拟工作区、固定 tick、最小恢复 | WASM/原生 ABI 测试、协议 schema、journal/checkpoint | create/load/step/snapshot/export/import/recover 通过 |
| M2 核心行为 | 解释器、canonical、TP、Task、motion、kinematics、工具服务 | 每个源组的适配报告、golden、差异报告 | 受影响核心条目 `verified`;blocked 输入已处理 |
| M3 仿真系统 | HAL、`.comp`、ClassicLadder、IO、数字孪生、Python/remap | 组件 manifest、HAL graph、故障矩阵、Python 能力报告 | 配置启动和 S1-S6 场景通过 |
| M4 Web 工作流 | 工程、编辑器、MDI、控制台、HAL/PLC、VCP、Three.js、导入导出 | 浏览器 E2E、截图/像素、性能和恢复证据 | 所有真实 Worker 工作流通过,mock 退出 |
| M5 发布闭合 | 全量 configs/tests/unit_tests、浏览器、离线、迁移、许可证 | 兼容报告、排除项、离线包、回滚包 | G0-G5 全绿,所有计入条目 `accepted` |
每个阶段都按“输入冻结 → 失败测试 → 最小实现 → native/WASM 对照 → 故障/恢复 → 文档和 manifest → 门禁”执行。阶段间不得用“先做 UI、后补核心”绕过依赖。
## 9. 实施任务的最小交付包
每个子任务只能有一个主要结果,并在提交中同时更新:
1. 受影响 source/feature/task/test/oracle 的 ID 和 source SHA。
2. C ABI、协议、快照或工作区 schema 的版本和兼容策略(若有变化)。
3. 正常、边界、错误、恢复和离线测试命令及结果路径。
4. native/WASM 差异报告、性能/内存/配额影响和已知限制。
5. 回滚点、未完成输入、下一任务和 `nextTask.md` 指针。
示例任务标题应具体到单一结果,例如:`M3-RT-03b:限位与换刀故障注入可回放`,而不是“完善仿真设备”。任务若同时修改两个独立契约,先拆成契约任务和实现任务。
## 10. 停止线和风险处置
- 来源或测试输入缺失:停止并记录 `blocked`,由新的契约变更决定是否补充输入;不删样本、不改目录级状态。
- 原生/WASM 首个差异未定位:冻结受影响源组和 golden,不进入下一层集成。
- 协议/ABI/schema 破坏性变化:新建版本、迁移和双向测试;旧版本不能静默接受。
- Python、组件或 VCP 需要宿主能力:改为虚拟能力或明确 `REAL-EXCLUDED`,并测试拒绝路径。
- OPFS journal/checkpoint 损坏、配额不足或确认丢失:保留最近已提交版本,进入恢复或导出,不覆盖旧数据。
- 非确定性、随机故障或浏览器墙钟改变结果:禁止合并,固定种子、顺序、tick 和输入后重跑。
- 性能优化导致 golden 变化:按行为回归处理,不能以“更快”豁免差异。
## 11. 当前立即执行顺序
1. 按 `nextTask.md` 执行 `K0-INV-01`,处理旧 `M2-CORE-04` 的 kinematics 输入阻塞,先确认锁定提交中不存在声明路径,并保留报告。
2. 若要采用替代 oracle,先创建可审查的契约变更和新的 context lock;否则保持 blocked,不推进 HAL 节点。
3. 在 kinematics 契约闭合后,完成静态模块注册、`forward/inverse/limits` ABI、确定性状态哈希和失败路径测试。
4. 继续 `M2-CORE-05` 前,先让 native runner、WASM runner 和 `sim_step` 共享同一 tick/golden schema。
5. 只有 M2 核心条目达到 `verified`,才进入 HAL/数字孪生/OPFS 恢复;只有 M3 通过,才把 mock UI 替换成真实 Worker 工作流。
本文件完成的是实施规则和证据闭环,不改变当前任务指针,也不把尚未实现的 LinuxCNC 功能标为完成。

View File

@@ -3,20 +3,20 @@
**状态**:M0 已落地,M1 ABI 骨架已验证,LinuxCNC 功能移植尚未完成 **状态**:M0 已落地,M1 ABI 骨架已验证,LinuxCNC 功能移植尚未完成
**目标**:把既有技术路线收敛成可执行的实施顺序、完整性证明和发布前置条件 **目标**:把既有技术路线收敛成可执行的实施顺序、完整性证明和发布前置条件
**固定基线**:LinuxCNC `2.10.0~pre0`,提交 `239dcf4284762ad7e9c65d390bc5524d855c2f8e` **固定基线**:LinuxCNC `2.10.0~pre0`,提交 `239dcf4284762ad7e9c65d390bc5524d855c2f8e`
**关联文档**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)、[覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)、[源程序关联](linuxcnc-web-wasm-source-map.md)、[通信设计](linuxcnc-web-wasm-frontend-communication-design.md)、[执行策略](linuxcnc-web-wasm-execution-strategy.md)、[总控手册](linuxcnc-web-wasm-integration-playbook.md) **关联文档**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)、[完整移植实施蓝图](linuxcnc-web-wasm-full-port-implementation.md)、[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)、[覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)、[源程序关联](linuxcnc-web-wasm-source-map.md)、[通信设计](linuxcnc-web-wasm-frontend-communication-design.md)、[执行策略](linuxcnc-web-wasm-execution-strategy.md)、[总控手册](linuxcnc-web-wasm-integration-playbook.md)
本文件是开始编写程序前的单页入口。专项文档保留详细契约;本文件只固定真实基线、先后顺序、完成定义和阻断条件。 本文件是开始编写程序和恢复任务前的单页入口。完整移植实施蓝图进一步规定启动状态机、运行时依赖、tick 顺序和各域产物;专项文档保留详细契约。本文件只固定真实基线、先后顺序、完成定义和阻断条件。
## 1. 现状和不可误读项 ## 1. 现状和不可误读项
本仓库已经具备完整设计文档,但此前没有 Web/WASM 运行时代码。当前 M0/M1 产物如下: 本仓库已经具备完整设计文档,并已落地一组可独立验证的 Web/WASM 运行时切片;LinuxCNC 全功能运行时仍未闭合。当前 M0/M1 产物如下:
| 产物 | 当前事实 | 允许的结论 | | 产物 | 当前事实 | 允许的结论 |
|---|---|---| |---|---|---|
| `context-lock.json` | 锁定 LinuxCNC 提交、Emscripten 3.1.6、schema v1 和索引哈希 | 基线可复现 | | `context-lock.json` | 锁定 LinuxCNC 提交、Emscripten 3.1.6、schema v1 和索引哈希 | 基线可复现 |
| `coverage/source-index.jsonl` | 7,992 个来源文件,来自 docs/src/configs/tests/unit_tests/build | 来源扫描已开始,可审计 | | `coverage/source-index.jsonl` | 8,026 个来源文件,来自 docs/src/configs/tests/unit_tests/build | 来源扫描已开始,可审计 |
| `coverage/manifest.jsonl` | 7,992 个一对一种子条目,全部 `planned/needs_owner` | 没有来源被静默漏掉;不代表已实现 | | `coverage/manifest.jsonl` | 8,026 个一对一种子条目,全部 `planned/needs_owner` | 没有来源被静默漏掉;不代表已实现 |
| `wasm-core` | C ABI 生命周期、固定 tick、快照、导入导出骨架 | 工程边界可编译、可调用 | | `wasm-core` | C ABI 生命周期、固定 tick、快照、导入导出,以及 posemath/解释器/运动适配切片 | 工程边界可编译、可调用;不代表完整核心闭合 |
| `build-wasm`/`build-native` | WASM 模块生成,宿主 ABI 单测通过 | 构建链路可重跑 | | `build-wasm`/`build-native` | WASM 模块生成,宿主 ABI 单测通过 | 构建链路可重跑 |
以下能力目前都不能声明完成:rs274ngc、TP、motion、kinematics、HAL、ClassicLadder、Python/remap、OPFS、Worker、UI、Three.js、全量配置和浏览器回归。 以下能力目前都不能声明完成:rs274ngc、TP、motion、kinematics、HAL、ClassicLadder、Python/remap、OPFS、Worker、UI、Three.js、全量配置和浏览器回归。

View File

@@ -4,13 +4,15 @@
**用途**:统一功能完整性、任务执行、依赖、测试、接续和里程碑验收 **用途**:统一功能完整性、任务执行、依赖、测试、接续和里程碑验收
**固定基线**:LinuxCNC `2.10.0~pre0`,以 `linuxcnc/VERSION` 和 context lock 为准 **固定基线**:LinuxCNC `2.10.0~pre0`,以 `linuxcnc/VERSION` 和 context lock 为准
**架构路线**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md) **架构路线**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)
**完整实施蓝图**:[完整仿真机床实施蓝图](linuxcnc-web-wasm-full-port-implementation.md)
**运动学主线**:[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)
**源码目录**:[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md) **源码目录**:[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)
**前端/通信**:[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md) **前端/通信**:[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)
**覆盖验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md) **覆盖验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)
**治理规则**:[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md) **治理规则**:[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)
**最小任务与上下文**:[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md) **最小任务与上下文**:[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md)
本手册是五份设计文档的总控入口。它不替代各专项文档,而是规定它们如何组合、谁负责什么、任务如何进入执行、依赖如何推进、失败如何恢复以及什么证据才能声明移植完成。 本手册是专项设计文档的总控入口。完整实施蓝图规定单个仿真入口如何启动和推进;本手册不替代这些专项文档,而是规定它们如何组合、谁负责什么、任务如何进入执行、依赖如何推进、失败如何恢复以及什么证据才能声明移植完成。
## 1. 当前审计结论 ## 1. 当前审计结论
@@ -19,14 +21,14 @@
```text ```text
设计基线:已建立 设计基线:已建立
源码/功能清单:`coverage/source-index.jsonl` 和 `coverage/manifest.jsonl` 已生成,全部条目仍待归并 源码/功能清单:`coverage/source-index.jsonl` 和 `coverage/manifest.jsonl` 已生成,全部条目仍待归并
运行时实现:`wasm-core` ABI bootstrap 已编译,LinuxCNC 核心尚未接入 运行时实现:`wasm-core` ABI bootstrap 及部分 posemath/解释器/运动切片已编译,LinuxCNC 全量核心尚未接入
前端实现:未开始或尚未进入当前工作区 前端实现:未开始或尚未进入当前工作区
全功能验收:未开始,不得声明已完成 全功能验收:未开始,不得声明已完成
``` ```
这是当前交付边界:后续任务从 M1/WS-1 的 ABI、原生 runner、固定 tick 和工作区契约继续;M0 清单重生成和哈希校验仍是每次基线变化的前置步骤。任何演示页面、mock Worker 或单一三轴样例都不能把项目状态提前改成“全功能完成”。 这是当前交付边界:后续任务从 M1/WS-1 的 ABI、原生 runner、固定 tick 和工作区契约继续;M0 清单重生成和哈希校验仍是每次基线变化的前置步骤。任何演示页面、mock Worker 或单一三轴样例都不能把项目状态提前改成“全功能完成”。
上下文治理的可执行入口是 `scripts/context_packet.py`,测试是 `scripts/test_context_packet.py`,任务拆分和分层读取以[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md)为准。当前仓库没有运行时实现,但上下文生成和预算测试已经可以独立运行;后续 CI 应在任何任务证据包生成前调用该脚本。 上下文治理的可执行入口是 `scripts/context_packet.py`、`scripts/validate_task_pointer.py`、`scripts/validate_model_budget.py` 和 `scripts/model_stream.py`,测试是 `scripts/test_context_packet.py`、`scripts/test_model_stream.py` 及对应校验器单测,任务拆分和分层读取以[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md)为准。模型预算校验还必须传入当前请求的 `--overhead-tokens`;连续任务从最新包重建,不累加旧对话。请求前校验输入、固定开销、输出预留和安全余量,请求后用 `validate_provider_response` 校验 provider usage;流式断开、上下文超限、usage 异常和截断响应只写 metadata-only checkpoint,不拼回下一轮。当前仓库已有可独立验证的核心切片,但尚未形成完整 LinuxCNC 运行时;上下文生成、启动指针、模型请求预算和流式恢复测试已经可以独立运行;CI 应在任何任务证据包生成或模型请求前调用相应脚本。
## 2. 唯一追踪链 ## 2. 唯一追踪链
@@ -116,7 +118,7 @@ M0 / WS-0 基线、source map、manifest、context lock
| CORE-04 | TP/posemath/几何 | `src/emc/tp/**`、posemath | canonical、轨迹和容差报告 | | CORE-04 | TP/posemath/几何 | `src/emc/tp/**`、posemath | canonical、轨迹和容差报告 |
| CORE-05 | Task/NML/状态 | `src/emc/task/**`、nml_intf、libnml | message bus 和状态快照 | | CORE-05 | Task/NML/状态 | `src/emc/task/**`、nml_intf、libnml | message bus 和状态快照 |
| CORE-06 | Motion/homing/limit | `src/emc/motion/**` | jog、home、limit、following error | | CORE-06 | Motion/homing/limit | `src/emc/motion/**` | jog、home、limit、following error |
| CORE-07 | Kinematics 全集 | `src/emc/kinematics/**` | 静态模块、能力注册、姿态 golden | | CORE-07 | Kinematics 全集 | `src/emc/kinematics/**` | 按[K0-K8 运动学主线](linuxcnc-web-wasm-kinematics-migration-plan.md)执行;静态模块、能力注册、姿态 golden |
| CORE-08 | Tooldata/IO 基础 | `src/emc/tooldata/**`、iotask | 工具、主轴、换刀、IO 状态 | | CORE-08 | Tooldata/IO 基础 | `src/emc/tooldata/**`、iotask | 工具、主轴、换刀、IO 状态 |
### 5.3 WS-2/WS-3/WS-4 运行时集成 ### 5.3 WS-2/WS-3/WS-4 运行时集成
@@ -145,14 +147,30 @@ M0 / WS-0 基线、source map、manifest、context lock
| REG-04 | 浏览器/性能/视觉矩阵 | FE、COM-03 | Chromium/Firefox/Safari、P95/P99、截图/像素 | | REG-04 | 浏览器/性能/视觉矩阵 | FE、COM-03 | Chromium/Firefox/Safari、P95/P99、截图/像素 |
| REL-01 | 兼容、许可证、迁移、回滚和离线包 | 全部 REG | 发布候选证据包和签名报告 | | REL-01 | 兼容、许可证、迁移、回滚和离线包 | 全部 REG | 发布候选证据包和签名报告 |
### 5.5 运动学主线 K0-K8
运动学不再作为 CORE-07 的单个串行任务处理。K0-K8 是从来源审计到发布闭合的硬门,详细输入、算法族和测试矩阵见[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)。
| 主线阶段 | 解锁范围 | 对整体项目的门禁 |
|---|---|---|
| K0 | 源文件、符号、配置入口、native FK/IK 样本和缺失输入 | 未闭合时不得声明任何非 trivkins 配置可运行 |
| K1 | C ABI、pose/joint 映射、flags、数值/迭代策略 | 未闭合时 motion 只能保留 bootstrap,不得接入全量算法 |
| K2 | trivkins 全类型、重复轴、extra joints、corexy/rose/max/rotate | 未闭合时不得验收 axis/joint 模式、HAL 耦合或恢复 |
| K3 | 5 轴 TRT、SCARA/PUMA/Scorbot/GenSer | 未闭合时不得验收刀尖跟随、机器人和 5 轴可视化 |
| K4 | Delta、RotaryDelta、Tripod、Hexapod、Pentapod | 未闭合时不得验收并联机构、迭代正解和 Vismach |
| K5 | switchkins、userk/ugenser 静态扩展和能力注册 | 未闭合时不得加载切换或用户运动学配置 |
| K6 | TP→IK→motion→FK feedback、home/limit、IO/数字孪生 | 未闭合时 M3 运动故障和恢复场景不具备发布资格 |
| K7 | Three.js 姿态、全配置回归和浏览器故障矩阵 | 未闭合时 M4 UI 只能作为结构原型 |
| K8 | 兼容、迁移、许可证、来源和回滚闭合 | 未闭合时 M5 不能进入发布候选 |
## 6. 里程碑总表 ## 6. 里程碑总表
| 里程碑 | 入口条件 | 必须完成 | 退出证据 | 失败动作 | | 里程碑 | 入口条件 | 必须完成 | 退出证据 | 失败动作 |
|---|---|---|---|---| |---|---|---|---|---|
| M0 基线锁定 | 版本和范围确认 | INV-01..05、状态枚举、责任人 | source/manifest/context lock 可重建,零孤儿规则生效 | 返回清单阶段,不写运行时代码 | | M0 基线锁定 | 版本和范围确认 | INV-01..05、状态枚举、责任人 | source/manifest/context lock 可重建,零孤儿规则生效 | 返回清单阶段,不写运行时代码 |
| M1 运行时骨架 | M0 通过 | CORE-01/02、RT-01、FS-01、COM-01/02 最小集 | create/load/command/step/snapshot/recover、离线缓存启动 | 固定 ABI/schema,修复后重跑 M1 | | M1 运行时骨架 | M0 通过 | CORE-01/02、RT-01、FS-01、COM-01/02 最小集 | create/load/command/step/snapshot/recover、离线缓存启动 | 固定 ABI/schema,修复后重跑 M1 |
| M2 核心行为 | M1 通过 | CORE-03..08、native/WASM runner、解释/轨迹/运动 golden | 核心 manifest `verified`,首个差异 tick 可定位 | 回退到对应核心任务,不进入 UI 全量 | | M2 核心行为 | M1 通过,K0 入口审计完成 | CORE-03..08、K0-K5、native/WASM runner、解释/轨迹/运动 golden | 核心 manifest `verified`,每个运动学族的首个差异 tick 可定位 | 回退到对应 K/核心任务,不进入 UI 全量 |
| M3 虚拟设备和恢复 | M2 通过 | RT-02..04、FS-02/03、PY-01/02、COM-03..05 | HAL/PLC/IO/故障/配额/重启/断网矩阵通过 | 阻断仿真会话发布,保留本地证据 | | M3 虚拟设备和恢复 | M2 通过,K6 完成 | RT-02..04、FS-02/03、PY-01/02、COM-03..05、运动学数字孪生 | HAL/PLC/IO/故障/配额/重启/断网矩阵通过 | 阻断仿真会话发布,保留本地证据 |
| M4 前端工作流 | M3 通过 | FE-01..10、COM-06、关键 UI/3D/配置工作流 | UI 条目有协议、源文件、测试、oracle 和恢复证据 | 禁止 mock 冒充完成,回退 FE/COM | | M4 前端工作流 | M3 通过 | FE-01..10、COM-06、关键 UI/3D/配置工作流 | UI 条目有协议、源文件、测试、oracle 和恢复证据 | 禁止 mock 冒充完成,回退 FE/COM |
| M5 发布候选 | M0..M4 通过 | REG-01..04、REL-01 | G0-G5 全绿、全计入条目 `accepted`、排除项可识别 | 维持候选冻结,创建修复 context 或回退 | | M5 发布候选 | M0..M4 通过 | REG-01..04、REL-01 | G0-G5 全绿、全计入条目 `accepted`、排除项可识别 | 维持候选冻结,创建修复 context 或回退 |

View File

@@ -0,0 +1,304 @@
# LinuxCNC Web/WASM 运动学完整移植计划
**文档状态**:重梳理版 v1.0
**适用基线**:LinuxCNC `2.10.0~pre0`,以 [`context-lock.json`](../context-lock.json) 为准
**目标**:完成所有纳入 `configs/sim/**` 的运动学类型、配置变体、正解/逆解、回零、模式切换和浏览器可视化闭环
**上位实施**:[完整仿真机床实施蓝图](linuxcnc-web-wasm-full-port-implementation.md)
**源程序关联**:[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)
**覆盖验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)
本文件将运动学从“CORE-04 的一个任务”提升为贯穿核心、运动、HAL、数字孪生、可视化和回归的主线。它不把算法按文件名简单罗列,而是要求每个类型都闭合:配置选择、参数解析、坐标/关节映射、正解、逆解、分支标志、回零、限制/奇异性、固定 tick、故障注入、native/WASM oracle、UI 姿态和恢复。
## 1. 为什么运动学必须单独成线
LinuxCNC 的 `KINEMATICS` 不只是一个 `forward()`/`inverse()` 函数:
- `KINEMATICS_IDENTITY`、`FORWARD_ONLY`、`INVERSE_ONLY`、`BOTH` 会改变 Task、motion、jog、home 和世界/关节模式转换。
- `coordinates=` 可以重复轴字母(如 `XYYZ`),额外关节不进入 kinematics;映射错误会让解释器正确的轨迹驱动错误的关节。
- 正解和逆解可能有多分支,需要 `KINEMATICS_FORWARD_FLAGS`、`KINEMATICS_INVERSE_FLAGS` 保持姿态连续;并行机构和机器人还需要上一次 pose 作为迭代初值。
- 许多配置使用 `switchkins`、HAL 实现的 CoreXY 或 GUI 专用 kinematics,不能只按 `[KINS]KINEMATICS` 的模块名判断。
- 5 轴刀尖跟随、转台/摆头、Delta/Hexapod、SCARA/PUMA 等算法的单位、角度、奇异点和收敛策略不同;“能编译”不代表“可运行”。
因此运动学主线是 M2 核心行为进入 M3 HAL/数字孪生和 M4 UI 的硬门。运动学主线未达到 `verified`,不得把任意非 trivkins 配置标为可运行。
## 2. 锁定来源和类型清单
### 2.1 公共接口和适配基础
必须逐文件登记并保留 `source_sha`:
| 来源组 | 文件 | 作用 |
|---|---|---|
| ABI/类型 | `src/emc/kinematics/kinematics.h` | `EmcPose`、四种 kinematics 类型、正逆解 flags、home 和 switch API |
| 映射/通用 | `kins_util.c`、`userkfuncs.c`、`ugenserkins.c` | `coordinates` 到 joint 映射、identity 实现、用户模板 |
| 切换 | `switchkins.c`、`switchkins.h` | 0/1/2 类型选择、last pose、迭代正解和 GUI pins |
| 几何 | `src/libnml/posemath/**`、`src/libnml/gomath/**` | 变换、矩阵、向量、数值容差和姿态表示 |
| 运动耦合 | `src/emc/motion/control.c`、`command.c`、`motion.c`、`homing.c` | kinematics 类型如何影响 mode、home、限位、following error 和实际位置 |
### 2.2 LinuxCNC 实际运动学实现族
下表是代码级实现族,不等同于配置数量。每个文件都要进入 manifest;同一算法的 `.c/.cc` 变体不能只保留一个目录状态。
| 族 | 实现文件 | 几何类别 | 典型配置入口 |
|---|---|---|---|
| 笛卡尔/映射 | `trivkins.c`、`maxkins.c`、`rotatekins.c` | 轴与关节同构、最大轴/旋转映射、identity/both 变体 | `axis/*.ini`、`qtaxis/xyz`、`gmoccapy/*axis*` |
| 耦合线性 | `corexykins.c`、`rosekins.c` | 线性关节耦合、双电机/特殊并联映射 | `axis/corexy/*`、`axis/rose_engine/*` |
| 泡沫/扩展轴 | `5axiskins.c`、`trivkins` 重复轴/extra joints | XYZ 与 UVW、旋转轴、非 kins extra joints | `axis/foam/*`、9 轴/extra-joints 配置 |
| 转台/摆头 | `trtfuncs.c`、`xyzac-trt-kins.c`、`xyzbc-trt-kins.c` | table-rotary-tilting、刀尖/偏置变换 | `axis/vismach/5axis/*`、`gmoccapy/non_trivial_kinematics/*` |
| Delta | `lineardeltakins.c/.cc`、`lineardeltakins-common.h` | 线性 Delta 正逆解、工作空间限制 | `axis/ldelta*`、`tklinuxcnc/ldelta.ini` |
| Rotary Delta | `rotarydeltakins.c/.cc`、`rotarydeltakins-common.h` | 旋转 Delta 几何 | `axis/rdelta.ini` |
| Tripod/并联 | `tripodkins.c` | 三腿并联、姿态分支 | `tklinuxcnc/tripod.ini` |
| Hexapod | `genhexkins.c`、`genhexkins.h` | 六杆并联、迭代正解、平台上下分支 | `axis/vismach/hexapod-sim/*` |
| Pentapod | `pentakins.c/.h` | 五杆并联、平台姿态和分支 | `axis/pentapod/*` |
| 通用串联机器人 | `genserkins.c`、`genserfuncs.c`、`genserkins.h` | DH 链、Jacobian、迭代逆/正解 | `qtaxis/non-trivial/scara` 及机器人样例 |
| SCARA | `scarakins.c` | 平面串联、腕部旋转、分支选择 | `axis/vismach/scara/*`、`qtvcp_screens/non-trivial/scara/*` |
| PUMA | `pumakins.c` | 6 轴串联、腕部奇异性和分支 | `axis/vismach/puma/*` |
| Scorbot | `scorbot-kins.c` | 机器人专用几何和姿态分支 | `configs/sim` 中 Scorbot/robot 资源 |
| 用户/示例 | `cubic.c/.h`、`userkfuncs.c`、`ugenserkins.c` | 用户参数和模板;不允许动态宿主插件 | 只在 manifest 明确登记后启用 |
| 切换容器 | `switchkins.c` | 多算法运行时切换 | SCARA、Hexapod 和含 switchkins 的配置 |
除了 `KINEMATICS` 值,还要扫描 `HALFILE` 中的 `corexy_by_hal`、`xyzab_tdr_kins`、`loadrt` 和 GUI pin;这些可能在 HAL 或用户组件层实现变换。
### 2.4 K0-INV-01 闭合审计(2026-08-20)
`coverage/kinematics-source-closure.report.json`(schema `v2`)是本节点的机器可读证据,并以当前 `context_id` 绑定到锁定来源。报告不只按文件名计数,而是把 source index、source-symbols、配置行和 HAL 行转成稳定 ID:
| 审计面 | 结果 | 证据字段 |
|---|---:|---|
| `src/emc/kinematics` 全目录文件 | 38 个,其中 35 个 `.c/.cc/.h` 实现/头文件 | `source.files[*].source_id`、`extension`、`sha256` |
| `KINEMATICS=`(`.ini` 和 `.inc`) | 268 条 | `configuration.entries[*].config_id` |
| `loadrt`、HAL 变换、switchkins 控制 | 111 条;模板 `[KINS]KINEMATICS`、`corexy_by_hal`、`motion.switchkins-type` 分别保留 | `hal.entries[*].config_id`、`kind`、`family` |
| 实现族 | 11 族均有源码 ID 和配置候选映射 | `families[*].source_ids`、`configuration_ids` |
| 测试输入 | `tests/matrixkins` 6 个文件;`linuxcnc/unit_tests/kinematics` 缺失 | `tests.*`、`missing_required_test_roots` |
| FK/IK oracle | 11 族全部 `blocked` | `families[*].oracle.reason`、`closure.oracle_blocked_families` |
缺失的官方测试根目录来自锁定提交本身;报告不会把 `tests/matrixkins/**` 改名或升级为 `unit_tests/kinematics/**`。`matrixkins` 仍作为 HAL 组件/配置入口候选登记,待契约变更或补充官方输入后才能产生 native FK/IK golden。2 个 TRT `.c` 文件在现有 source-symbols 报告中是 `indexed_empty`,不是静默丢失;其 source ID、哈希和待补符号状态均保留。
### 2.5 K0-INV-02 配置图与样本集(2026-08-20)
`coverage/kinematics-config-graph.report.json` 将审计报告中的 262 个 `KINEMATICS` 条目和 111 个 HAL/切换条目展开为 373 个入口节点、1,491 个文件/族/资源节点和 2,774 条有向边。模板化 `loadrt [KINS]KINEMATICS` 的解析状态分为 `explicit_reference`、`directory_owner` 和 `inactive_reference`;后两类保留来源事实,但不能当作已启动配置。图的 `orphan_entry_ids` 和 `unowned_template_ids` 必须为空。
`coverage/kinematics-oracle-samples.jsonl` 为 11 个实现族各登记一个固定 `fk_ik_fk_roundtrip` 样本、候选配置、输入姿态/关节、容差和精确字段。样本当前全部 `blocked`,原因仍是锁定提交没有 `linuxcnc/unit_tests/kinematics/**`;`tests/matrixkins/test.ini` 只作为 `user_static` 的配置候选,未被提升为官方 oracle。只有补充/批准输入契约后,才允许把 `execution.status` 改为可运行并生成 native golden。
### 2.6 K0-ORACLE-01 runner/schema(2026-08-20)
`scripts/generate_kinematics_oracle_report.py` 在固定 `PATH/HOME/LANG/LC_ALL/TZ/PYTHONHASHSEED` 环境中运行 `build-native/sim_kinematics_test` 作为 native smoke,并为 11 个样本输出 `coverage/kinematics-oracle.report.json`。`coverage/kinematics-golden-schema.json`(`linuxcnc-web-wasm/kinematics-golden.v1`)要求 `status/error_code/kinematics_type/branch_id/flags/current_tick/config_hash/axis_joint_mapping` 等字段;缺字段不会通过比较。当前 native smoke 为 `passed`,但 11 个样本均为 `blocked`,错误码为 `MISSING_OFFICIAL_ORACLE`,没有伪造数值 golden。
### 2.7 K1-CONTRACT-01 ABI contract(2026-08-20)
`coverage/kinematics-abi-contract.json` 从 `sim_kinematics.h`、`wasm-core/CMakeLists.txt` 和固定数值策略生成。它锁定 ABI version 1、8 个公开函数、结果/类型/mode 枚举、3 个结构体字段列表、`SIM_KIN_ERR_*` 错误码、`-ffp-contract=off`/`-fno-fast-math`、非有限值拒绝、100000 最大迭代和 FNV-1a-64 state hash;缺失 Emscripten export 会使 contract `blocked`。当前 contract `verified`,native/WASM 构建及 8 个 CTest 均通过。
`coverage/kinematics-mapping-contract.json` 进一步锁定 ASCII case-fold、重复轴 first-wins、extra-joint 数量、home 零位、inclusive limits、forward/inverse 映射和 state hash 输入字段;`sim_kinematics_test` 已覆盖 `XYYZ`、越界和 home 样本。
`coverage/kinematics-numeric-policy.json` 固定 `-ffp-contract=off`、禁用 fast-math、角度单位、100000 迭代上限、1e-12 奇异阈值、1e-9 golden 容差和 exact 字段;NaN/Inf、负容差、超迭代、奇异和越界都列入边界矩阵。
### 2.3 当前 WASM 雏形的真实边界
当前 `wasm-core/include/sim_kinematics.h`/`sim_kinematics.c` 只能作为 K1 之前的实验切片,不能作为完整实现证据:
| 已存在 | 尚未闭合 |
|---|---|
| `trivkins`、`XYZAC-TRT`、`XYZBC-TRT`、`XYZBCW` 的有限正逆解和基础 joint limit 检查 | LinuxCNC 全部实现族:CoreXY、Rose、5axiskins 完整模式、Delta/RotaryDelta、Tripod、Hexapod、Pentapod、GenSer、SCARA、PUMA、Scorbot、switchkins 和用户静态扩展 |
| 基础 `coordinates` 字符串和重复关节填充 | `KINEMATICS_TYPE` 四种语义、extra joints、完整轴 bitmap、world/joint mode 约束和 home 语义 |
| 有限性检查、少量奇异性检查和 FNV 状态 hash | `fflags/iflags` 分支状态、last-pose/seed、residual、iterations、branch_id、确定性迭代策略和恢复字段 |
| C API 单元样例 | LinuxCNC native FK/IK oracle、每个配置入口的 golden、HAL 实现等价性和 UI/Vismach 姿态证据 |
另外,现有 `sim_kinematics.c` 尚未加入 `wasm-core/CMakeLists.txt` 的 `sim_core`/Emscripten module,`sim_kinematics_test.c` 也未进入 CTest;因此当前 `ctest` 的 7/7 通过不包含运动学验证。K1 的第一项集成工作必须先修复构建和测试注册,再谈算法族扩展。
因此 K0/K1 不能只围绕现有 `sim_kinematics_*` 函数补测试;必须先完成统一契约,再按 K2-K5 族逐步替换或扩展实现。现有切片的错误码和字段若与统一 ABI 冲突,应通过版本化契约迁移,不得在旧 ABI 上堆叠不可兼容的类型。
## 3. 统一运动学契约
### 3.1 输入、输出和状态
WASM 内部使用固定宽度、明确大小的结构,TypeScript 不接触 C 结构体布局。逻辑契约如下:
```text
KinematicsRequest {
kind: FORWARD | INVERSE | HOME | CHECK
joints[N] / pose{ x,y,z,a,b,c,u,v,w }
fflags: uint64
iflags: uint64
seed_pose: optional pose # 迭代正解初值
config_hash: bytes32
simulation_tick: uint64
}
KinematicsResult {
status: OK | INVALID_INPUT | OUT_OF_RANGE | SINGULAR | NO_SOLUTION |
NOT_CONVERGED | UNSUPPORTED | NUMERIC_ERROR
joints[N] / pose
fflags: uint64
iflags: uint64
residual: float64
iterations: uint32
branch_id: uint32
state_hash: bytes32
}
```
必须固定以下语义:
1. `forward(joints)` 返回世界 pose,并更新适用的 `iflags`;`inverse(pose)` 返回关节,并更新适用的 `fflags`。
2. flags 是会话状态的一部分,必须进入快照、golden 和恢复账本;不能每次调用清零来掩盖分支变化。
3. `home()` 产生已知的世界 pose、joint 和 flags;不能只把所有关节设为零。
4. `KINEMATICS_FORWARD_ONLY`、`INVERSE_ONLY` 的 mode 转换限制要与 LinuxCNC 一致;不支持的调用返回 `UNSUPPORTED`,不能返回伪造 pose。
5. 所有输入输出先检查有限性、维数、单位和配置 hash;NaN/Inf、数组越界、未知轴字母和未知类型在进入算法前拒绝。
### 3.2 坐标、关节和 extra joints
`coordinates` 解析为轴索引到 joint 编号的稳定映射,并记录重复轴 bitmap。运动学 joint 数量与 `EMCMOT_MAX_JOINTS`、`NO_OF_KINS_JOINTS`、extra joints 的边界必须分开表示:
- kinematics 只接收前 `NO_OF_KINS_JOINTS` 个关节;extra joints 由 motion/Task 管理,不得误传给算法。
- `XYYZ`、`XYZY` 等重复坐标必须在配置加载时生成映射 golden,并在 joint/world 模式切换时复用。
- 旋转轴的角度单位、绕轴方向、回绕策略和连续性要逐类型登记;不能用统一的线性限位代码代替。
- UI 显示的 axis pose、joint pose、commanded/actual pose 必须注明坐标系和来源 tick。
### 3.3 正逆解数值策略
解析解与迭代解使用同一个数值政策:
- Emscripten/native 固定编译选项、舍入模式和 `sin/cos/sqrt/atan2` 包装;算法不得调用墙钟或未锁定随机数。
- 解析算法返回工作空间、关节限位、姿态范围和分支错误,而不是将超界结果钳位后返回 `OK`。
- 迭代算法固定初值来源、最大迭代次数、收敛残差、步长/阻尼、奇异阈值和失败码;每次结果记录 `iterations/residual/branch_id`。
- 对正逆解只使用定义好的几何容差;状态、分支、错误码和事件顺序必须精确一致。
- 迭代正解(Hexapod、通用串联等)在相邻 tick 使用上一次有效 pose,但恢复后必须从 checkpoint 中恢复该 seed。
## 4. 按算法族的实施要求
### 4.1 笛卡尔和耦合线性族
先完成 `trivkins` 的 identity/both/inverse-only/forward-only、坐标重复映射和 max/rotate 变体,再完成 `corexykins`、`rosekins` 的矩阵映射。每一类需要:
- 正逆解矩阵和符号方向的单元向量测试;
- 重复轴、extra joints、关节限位、回零和 joint/world 模式测试;
- HAL 实现的 `corexy_by_hal` 与 kinematics 模块实现的对照,证明同一轨迹在两条链上等价;
- 快速/1x/暂停恢复后的关节和世界 pose hash 一致。
当前 K2 耦合切片已完成 CoreXY、Rose、MaxKins 和 RotateKins 的统一实现。CoreXY 复现 LinuxCNC 的半和/半差公式,并分别登记 `corexy_by_kins.ini` 与 `corexy_by_hal.ini`,HAL 组件路径保持为独立入口;Rose inverse 保存上一象限和累计圈数,跨 2→3 象限得到连续角度和稳定 branch;MaxKins 保留 pivot/B/C/U/V/W 修正公式并覆盖真实 `max5kins.ini` 的 `pivot-length=0`;RotateKins 覆盖负 C 旋转方向和 90°边界。锁定 LinuxCNC 没有 shipped 的 rotatekins `.ini`,因此新增的 `linuxcnc/configs/sim/axis/rotate/rotatekins.ini` 明确标为 workspace-derived fixture,不冒充官方配置或 oracle。`coverage/coupled-kinematics.report.json` 使用相同样本运行 native 与 WASM,并比较规范化 JSON 和 state hash;由于 `linuxcnc/unit_tests/kinematics` 不在锁定源中,报告状态仍为 partial/blocked,不生成伪造官方 golden。
`K2-HAL-01` 已在同一契约上接入静态 `corexy_by_hal`。SimHAL 注册组件的 8 个 float pin 和 servo-thread function,周期内执行 `.comp` 的 `alpha/beta` 命令矩阵与 `j0/j1` 反馈矩阵;HAL 配置中的 `j0home/j1home` 路由在变换前保留 Cartesian home 语义,并与 `corexykins` 的 motor-joint home 明确区分。`coverage/corexy-hal.report.json` 对配置连接、公式、native/WASM 输出和 oracle 阻塞状态做确定性检查;完整 HAL 命令解析、其它组件和真实 unit-test oracle 仍属于后续节点。
`K3-5AXIS-01` 已接入 `5axiskins` 的 XYZBCW spherical pivot 链。真实 `bridgemill/5axis.ini` 使用 `coordinates=xyzbcwy` 和七个 joints,重复 Y 由 first-wins 映射并在 inverse 后同步 suffix;pivot length 250、B/C 角度和 W 工具运动均进入 FK/IK 与 state hash。缺少 XYZBCW 任一必需字母返回 configuration error;`coverage/fiveaxis-kinematics.report.json` 对配置、源公式、重复轴、边界和 native/WASM 样本做确定性记录,官方 unit-test oracle 仍 blocked。
`K3-TRT-01` 已接入 `xyzac-trt-kins` 与 `xyzbc-trt-kins` 共用的 `trtfuncs.c` 姿态链。`table-rotary-tilting/xyzac-trt.ini` 与 `xyzbc-trt.ini` 的 `sparm=identityfirst`、五关节、tool/offset HAL pins 均登记;XYZAC 保持 C→A 顺序,XYZBC 保持 C→B 顺序,offset 与 tool length 进入固定解析式。B=90 的配置边界显式返回 singular,native/WASM 样本和 hash 见 `coverage/trt-kinematics.report.json`;switchkins 运行时切换容器留到 K5,不在本节点伪装完成。
`K3-ROBOT-01` 已接入 SCARA、PUMA 和 Scorbot 专用解析式。SCARA 使用 D1-D6、C 总姿态和 elbow 分支;PUMA 使用 A2/A3/D3/D4/D6、完整旋转矩阵/RPY 转换以及 shoulder/elbow/wrist 分支和 wrist singular 检查;Scorbot 使用固定 L0/L1/L2 链和受控点 RZ 逆解。SCARA 与 PUMA 取自 shipped Vismach 配置,Scorbot 因锁定源没有 shipped INI 使用明确标注的 workspace-derived fixture。PUMA 等复杂三角函数的 native/WASM state hash 在 `1e-9` golden 容差上规范化,避免后端 libm 末位差异成为伪状态分叉;所有不可达/奇异边界和官方 oracle 状态见 `coverage/robot-kinematics.report.json`。
`K3-GENSER-01` 已接入六关节 Craig DH 链和固定零初值的阻尼数值 Jacobian inverse。DH `A/ALPHA/D` 数组、alpha 弧度与 joint 度数转换、有限差分步长、阻尼、最大迭代次数、residual 和不收敛错误均进入配置/报告;melfa 与 puma560 的真实 `genserkins` 配置作为候选,样本在 native/WASM 逐字一致。此节点不把动态 `switchkins` 或用户库放行到浏览器,官方 unit-test oracle 仍 blocked,详见 `coverage/genser-kinematics.report.json`。
`K4-DELTA-01` 已接入 linear/rotary Delta 的统一 forward/inverse/mapping 路径。线性 Delta 使用锁定 `lineardeltakins-common.h` 的 120 度塔架与杆长解析二次式;旋转 Delta 使用锁定 `rotarydeltakins-common.h` 的平台/大腿/小腿/足端几何。两者均固定 XYZ 到前三个 kinematics joints 的映射,A/B/C/U/V/W 透传,额外 joints 不进入算法;默认根保持 LinuxCNC 的平台根,`SIM_KIN_DELTA_ALTERNATE_ROOT` bit 0 使 alternate root、`branch_id` 和 state hash 可观察。真实 `ldelta.ini`、`ldelta_demo.ini`、`tklinuxcnc/ldelta.ini`、`rdelta.ini` 均登记;nominal/alternate root、工作空间无解、关节限位和 rotary `z=0` 奇异边界在 native/WASM 逐字对照。配置引用的 HAL 文件在锁定工作区缺失,作为 resource gap 保留,不能升级为配置 smoke 或数值 golden;官方 `linuxcnc/unit_tests/kinematics/**` 同样缺失,全部 Delta oracle 明确 blocked,详见 `coverage/delta-kinematics.report.json`。
### 4.2 5 轴转台/摆头族
`5axiskins`、`xyzac-trt-kins`、`xyzbc-trt-kins` 必须共享但不能混用各自的旋转顺序、刀尖点和 offset 语义。验收至少包括:
- A/B/C 旋转顺序、正负方向、角度单位和回绕;
- table-rotary 与 tool-rotary 的姿态链;
- `identityfirst` 参数、刀长/刀尖偏置、G43/G43.1/G49 与逆解;
- 旋转轴接近 0/180/360 度、万向节/姿态奇异和无解报警;
- impeller、table-rotary-tilting、dual-rotary 的真实 `configs/sim` 入口和 Vismach 姿态。
### 4.3 Delta、Tripod、Hexapod、Pentapod
并联机构不能只测“一个可达姿态”。每种机构需要工作空间边界、腿长/关节范围、平台上下分支、奇异面和迭代初值测试:
- Delta:塔架几何、臂长、平台平移边界、负根/无根和线性/旋转变体;
- Tripod:三腿约束、姿态分支、回零和连续轨迹;
- Hexapod:六杆长度、平台上下姿态、迭代正解、last-pose 和 GUI pins;
- Pentapod:五杆约束、平台姿态、不可达/奇异区域和 `pentapod.var` 参数。
每次迭代必须输出 residual 和迭代次数。相同 seed、输入和 tick 必须得到相同分支;越过分支边界时要产生可解释的 `SINGULAR` 或 `NO_SOLUTION` 事件,不能突然跳姿态。
### 4.4 通用串联机器人族
`genserkins` 以 DH 链、Jacobian 和迭代求解为核心;`scarakins`、`pumakins`、`scorbot-kins` 是专用参数化实现。实施顺序:
1. 统一 link 参数、角度单位、joint offset、正方向和关节范围。
2. 先用 FK 生成 pose oracle,再用 FK→IK→FK round-trip 验证每一个分支。
3. 对 Jacobian 条件数、阻尼、步长、初值和最大迭代建立配置字段,全部进入快照。
4. 覆盖 elbow-up/down、腕部翻转、SCARA 平面无解、PUMA wrist singularity 和 Scorbot 专用姿态。
5. UI 轨迹显示必须消费 kinematics 输出的姿态,不能用线性 axis 拼接替代机器人 FK。
### 4.5 Switchkins 和用户扩展
`switchkins` 不是动态加载的放行口,而是静态注册的最多三种实现容器。每个 `switchkins_type` 必须有:
- 类型、坐标要求、最大 joint 数、是否允许重复轴、是否迭代正解的 manifest;
- 切换前后同步点、flags/lastpose 初始化和 GUI pin;
- 非法类型、缺失实现、切换中运动、恢复后重建和回滚测试。
`userkfuncs`、`ugenserkins` 和 `.so/.so` 类宿主动态模块不能直接加载。可移植用户运动学必须在构建期编译为静态 WASM 模块,提供相同 ABI、版本和 capability;不满足时返回 `UNSUPPORTED_USER_KINS` 并阻断配置启动。
## 5. 运动学主线任务和里程碑
M0/M1 的基线和会话骨架保持不变;从原 `M2-CORE-04` 起,采用以下 K 系列主线。K 系列任务完成后才能解锁对应的 M3/M4 工作流。
| 里程碑 | 任务 | 结果 | 依赖 | 退出门槛 |
|---|---|---|---|---|
| K0 清单与 oracle | `K0-INV-01` 源文件/符号索引;`K0-INV-02` 配置入口图;`K0-ORACLE-01` native FK/IK/golden schema | 完整类型、参数和配置清单 | M0 | 所有 kinematics 文件和入口有 source/feature/test/oracle;缺失输入显式 blocked |
| K1 ABI 与数值 | `K1-CONTRACT-01` C ABI;`K1-CONTRACT-02` flags/pose/映射;`K1-NUM-01` 浮点/迭代政策 | `sim_kinematics.h`、错误码、状态 hash | K0、M1 | 同输入三次字节和 hash 一致;非法/NaN/Inf/越界全有测试 |
| K2 笛卡尔/耦合 | `K2-TRIV-01` trivkins 全类型;`K2-TRIV-02` 重复轴/extra joints;`K2-COUPLED-01` corexy/rose/max/rotate;`K2-HAL-01` HAL 等价链 | identity、both、耦合线性运动学 | K1 | 轴/关节模式、home、限位、HAL 和恢复 golden 全绿 |
| K3 5 轴串联 | `K3-5AXIS-01` 5axiskins;`K3-TRT-01` xyzac/xyzbc/trt;`K3-ROBOT-01` SCARA/PUMA/Scorbot;`K3-GENSER-01` DH/Jacobian | 5 轴、转台/摆头和串联机器人 | K2、posemath | FK/IK round-trip、分支、奇异、刀尖/刀长和真实配置入口通过 |
| K4 并联机构 | `K4-DELTA-01` linear/rotary Delta;`K4-TRIPOD-01` tripod;`K4-HEX-01` genhex;`K4-PENTA-01` pentapod | 全部并联正逆解和迭代恢复 | K1、K2 | 工作空间边界、无解、奇异、分支、lastpose、GUI pins 和故障注入通过 |
| K5 切换/扩展 | `K5-SWITCH-01` switchkins;`K5-USER-01` 静态 userk/ugenser;`K5-CFG-01` 参数和能力注册 | 可切换和构建期扩展模型 | K3、K4 | 非法切换、缺模块、恢复、版本和权限错误明确;无动态宿主加载 |
| K6 运动集成 | `K6-MOTION-01` TP→IK→motion;`K6-MOTION-02` FK feedback;`K6-HOME-01` home/limit;`K6-IO-01` spindle/tool/probe | 运动学与 Task/HAL/IO/数字孪生闭环 | K2-K5 | `configs/sim` 运动学入口可启动、运行、暂停、急停、恢复 |
| K7 UI/回归 | `K7-VIS-01` 姿态/关节/刀尖渲染;`K7-REG-01` 全配置;`K7-REG-02` 浏览器故障矩阵 | 可视化和全量回归 | K6、M3/M4 | 轨迹、姿态、joint/world、报警、回放和多浏览器证据齐全 |
| K8 发布闭合 | `K8-REL-01` 兼容报告;`K8-REL-02` 迁移/回滚;`K8-REL-03` 许可证/来源闭包 | 运动学纳入完整仿真机床发布 | K0-K7 | 受影响条目全部 `accepted`,无孤儿、无未解释差异、无静默降级 |
`K4-TRIPOD-01` 已完成:`tripodkins.c` 的点顶点三杆并联机构已接入统一 forward/inverse/mapping,使用真实 `linuxcnc/configs/sim/tklinuxcnc/tripod.ini` 和 `linuxcnc/lib/hallib/tripodsim.hal` 验证 XYZ/AD-BD-CD 映射、above/below 根分支、三角不等式无解、基面奇异、关节限位、退化基座和固定零迭代;native/WASM JSON 与 state hash 一致,官方 oracle 因锁定来源缺少 `linuxcnc/unit_tests/kinematics/**` 继续显式 blocked。证据见 `coverage/tripod-kinematics.report.json`。
`K4-HEX-01` 已完成:`genhexkins.c` 的六腿 Stewart 平台已接入统一 forward/inverse/mapping,使用真实 `linuxcnc/configs/sim/axis/vismach/hexapod-sim/hexapod.ini` 与 `kinematics.hal` 验证六腿闭式逆解、Newton 6x6 正解、确定性初始种子、alternate seed、非正 strut、迭代上限、最大误差、奇异 Jacobian、限位、HAL GUI/迭代 pin 和 native/WASM state hash;官方 oracle 继续显式 blocked。证据见 `coverage/genhex-kinematics.report.json`。
每个 K 里程碑都必须同时更新 `coverage/manifest.jsonl`、source map、golden 索引、任务指针和证据摘要。算法代码通过单测但没有配置入口或 UI 姿态证据时,不能推进到下一个里程碑。
## 6. 运动学测试矩阵
每个实现族至少有以下测试层;参数和样本由 K0 清单生成,不允许只挑“容易收敛”的点:
| 层 | 样本 | 必测内容 |
|---|---|---|
| 纯函数 | 合法 pose/joint、边界、随机种子固定的网格 | FK、IK、FK→IK→FK、IK→FK→IK、flags 和 residual |
| 几何边界 | 轴/关节最小最大、工作空间边缘、旋转回绕 | limit、无解、奇异、分支连续性 |
| 配置 | 每个非 trivkins `configs/sim` 入口及所有 `trivkins` 变体 | 参数、坐标、HALFILE、tool/var、资源图和启动错误 |
| 运动 | G0/G1/G2/G3、5 轴刀尖、jog、home、暂停、abort | TP→IK→motion、反馈、following error、最终姿态 |
| 故障 | 探针、限位、主轴/换刀、迭代不收敛、Python 异常 | 报警、停止、恢复、状态 hash 和事件顺序 |
| 持久化 | checkpoint、journal、刷新、Worker 重启、离线 | flags、lastpose、seed、工具和姿态恢复一致 |
| 浏览器 | Chromium/Firefox/Safari 计划矩阵、窄屏/触摸 | 3D 非空、姿态无跳变、背压、错误可见 |
### 6.1 Golden 和容差
native runner 为每个样本保存输入 hash、配置 hash、算法版本、pose/joint、flags、residual、iterations、branch、错误和最终 state hash。WASM 只允许在已登记的浮点字段使用容差;以下字段必须精确:
```text
status / error_code / kinematics_type / branch_id
flags / current_tick / event_order / config_hash
axis-to-joint mapping / home result presence / state transition
```
差异报告必须包含首个 divergent tick、字段路径、原生值、WASM 值、容差、source_id 和可重放命令。迭代不收敛不得被提高最大迭代次数或放宽容差“修绿”;应记录算法参数和失败原因。
## 7. 与整体仿真机床的接合顺序
运动学主线不是独立库,接合顺序固定如下:
1. K1 ABI 完成后,`sim_motion` 只通过 `KinematicsRegistry` 调用,不再硬编码 trivkins。
2. K2 完成后,解释器/TP 的 canonical 轨迹接入 axis/joint 变换,HAL 反馈接入 FK。
3. K3/K4 完成后,数字孪生为每种机床提供真实 joint/pose 反馈、限位、回零和故障;Vismach/Three.js 使用同一 FK 结果。
4. K5 完成后,Python/remap/VCP 获得只读 kinematics 状态和受控切换命令;切换必须经过 Task 同步点。
5. K6 完成后,OPFS checkpoint 保存 kinematics type、参数、flags、lastpose、seed 和 joint/pose 状态。
6. K7 完成后,UI 同时显示世界坐标、关节坐标、机床姿态、刀尖和限制原因;不能把 axis 数值当成所有机床的刀具姿态。
## 8. 当前执行和停止线
当前 `M2-CORE-04` 的 `unit_tests/kinematics/**` 缺失仍然是 K0 的输入审计事项,不自动改成实现任务。立即顺序为:
1. `K0-INV-01` 确认锁定提交的 kinematics 文件、符号和测试路径,生成缺失输入报告。
2. `K0-INV-02` 从 `configs/sim`、`tests/matrixkins` 和实际 kinematics 配置生成入口图;这些可以作为配置 oracle,但不能冒充缺失的官方 unit test。
3. `K0-ORACLE-01` 在原生 runner 中建立 FK/IK JSONL schema 和最小 trivkins/corexy/非平凡样本。
4. 经审查决定是否创建新的输入契约/context lock;未决定前保持 blocked,不跳到 HAL 节点。
5. K1 ABI 和数值政策完成后,再按 K2→K5 算法族推进;K6 以前不开放全量 UI 运动学声明。
以下条件必须停止并回到对应里程碑:输入源或配置入口遗漏;正逆解首次差异未定位;分支/flags/lastpose 未进入快照;算法依赖墙钟或随机数;用伪造 pose、钳位越界值、放宽容差或刷新重建掩盖失败;用户运动学需要未登记的动态宿主库。
完成 K8 后,运动学才能从“核心切片”升级为“仿真机床完整能力”,并作为整个 LinuxCNC Web/WASM 发布的必要条件。

View File

@@ -151,7 +151,7 @@
| 补偿 | cutter radius、tool length、dynamic tool length、additional tool offset、wear、螺距误差 | `SIM-CORE` | | 补偿 | cutter radius、tool length、dynamic tool length、additional tool offset、wear、螺距误差 | `SIM-CORE` |
| 主轴同步 | threading、G33/G33.1、rigid tapping、CSS、gear、orient、multiple spindle | `SIM-DEVICE` | | 主轴同步 | threading、G33/G33.1、rigid tapping、CSS、gear、orient、multiple spindle | `SIM-DEVICE` |
| 控制环 | PID、feed/velocity/position loop、encoder、stepgen、adaptive feed | `SIM-CORE` | | 控制环 | PID、feed/velocity/position loop、encoder、stepgen、adaptive feed | `SIM-CORE` |
| 运动学模块 | `trivkins`、`5axiskins`、`corexykins`、`cubic`、`genhexkins`、`genserkins`、`lineardelta`、`maxkins`、`pentakins`、`rosekins`、`rotarydelta`、`rotatekins`、`scarakins`、`scorbot`、`tripod`、`xyzac-trt`、`xyzbc-trt`、`switchkins`、`ugenser/userkfuncs` | `SIM-CORE` | | 运动学模块 | `trivkins`、`5axiskins`、`corexykins`、`cubic`、`genhexkins`、`genserkins`、`lineardelta`、`maxkins`、`pentakins`、`rosekins`、`rotarydelta`、`rotatekins`、`scarakins`、`scorbot`、`tripod`、`xyzac-trt`、`xyzbc-trt`、`switchkins`、`ugenser/userkfuncs` | `SIM-CORE`;实现族、正逆解、分支/奇异性和 K0-K8 门禁见[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md) |
| DH/机器人 | DH 参数、PUMA、SCARA、六足、Delta、Pentapod、Scorbot 和自定义静态 kinematics | `SIM-CORE` | | DH/机器人 | DH 参数、PUMA、SCARA、六足、Delta、Pentapod、Scorbot 和自定义静态 kinematics | `SIM-CORE` |
| 用户 kinematics | 构建期静态 WASM ABI、热切换由仿真状态机完成 | `SIM-ADAPTER` | | 用户 kinematics | 构建期静态 WASM ABI、热切换由仿真状态机完成 | `SIM-ADAPTER` |
| 坐标/姿态显示 | joints、axes、pose、forward/inverse、tool orientation 和 singularity 错误 | `WEB-EQUIV` | | 坐标/姿态显示 | joints、axes、pose、forward/inverse、tool orientation 和 singularity 错误 | `WEB-EQUIV` |

View File

@@ -44,7 +44,7 @@
| Worker 协议与固定 tick 边界 | `wasm-core/include/sim_protocol.h`、`wasm-core/src/sim_protocol.c`、`web/worker/sim-session.mjs`、`web/worker/sim-worker.mjs` | 版本化 envelope、单调 seq/tick、会话状态机和错误映射;WASM 核心只暴露受控 C ABI | `wasm-core/tests/sim_protocol_test.c`、`web/worker/test_wasm_session.mjs`、`scripts/test_protocol.py` | | Worker 协议与固定 tick 边界 | `wasm-core/include/sim_protocol.h`、`wasm-core/src/sim_protocol.c`、`web/worker/sim-session.mjs`、`web/worker/sim-worker.mjs` | 版本化 envelope、单调 seq/tick、会话状态机和错误映射;WASM 核心只暴露受控 C ABI | `wasm-core/tests/sim_protocol_test.c`、`web/worker/test_wasm_session.mjs`、`scripts/test_protocol.py` |
| RTAPI、OS 接口和时钟 | `linuxcnc/src/rtapi/**`;`linuxcnc/src/libnml/os_intf/timer.cc`、`_timer.c`、`sem.cc`、`shm.cc`;`linuxcnc/src/emc/linuxcnc.h` | 固定逻辑时钟、虚拟同步原语和受控内存;内核实时、CPU 绑定和设备访问标记为 `REAL-EXCLUDED` | `linuxcnc/tests/lowlevel/**`、`linuxcnc/tests/rtapi-shmem/**`(可移植部分) | | RTAPI、OS 接口和时钟 | `linuxcnc/src/rtapi/**`;`linuxcnc/src/libnml/os_intf/timer.cc`、`_timer.c`、`sem.cc`、`shm.cc`;`linuxcnc/src/emc/linuxcnc.h` | 固定逻辑时钟、虚拟同步原语和受控内存;内核实时、CPU 绑定和设备访问标记为 `REAL-EXCLUDED` | `linuxcnc/tests/lowlevel/**`、`linuxcnc/tests/rtapi-shmem/**`(可移植部分) |
| Motion、回零和限位 | `linuxcnc/src/emc/motion/motion.c`、`control.c`、`command.c`、`homing.c`、`emcmotglb.c`、`emcmotutil.c`、`simple_tp.c`、`motion_struct.h`、`homing.h` | `sim_motion` 状态机;虚拟编码器、开关、跟随误差和故障注入 | `linuxcnc/tests/hard-limits/**`、`linuxcnc/tests/abort/**`、`linuxcnc/tests/limit3/**` | | Motion、回零和限位 | `linuxcnc/src/emc/motion/motion.c`、`control.c`、`command.c`、`homing.c`、`emcmotglb.c`、`emcmotutil.c`、`simple_tp.c`、`motion_struct.h`、`homing.h` | `sim_motion` 状态机;虚拟编码器、开关、跟随误差和故障注入 | `linuxcnc/tests/hard-limits/**`、`linuxcnc/tests/abort/**`、`linuxcnc/tests/limit3/**` |
| Kinematics | `linuxcnc/src/emc/kinematics/*kins.c`、`*kins.cc`、`kins_util.c`、`genserfuncs.c`、`trtfuncs.c`、`switchkins.c`;关键文件包括 `trivkins.c`、`5axiskins.c`、`genhexkins.c`、`genserkins.c`、`lineardeltakins.c`、`rotarydeltakins.c`、`corexykins.c`、`xyzac-trt-kins.c`、`xyzbc-trt-kins.c` | 静态 WASM 模块和 manifest 注册;不使用宿主动态库 | `linuxcnc/configs/sim/axis/gantry/**`、`linuxcnc/configs/sim/axis/foam/**`、`linuxcnc/configs/sim/axis/ldelta*`、机器人样例;运动学单测 | | Kinematics | `linuxcnc/src/emc/kinematics/*kins.c`、`*kins.cc`、`kins_util.c`、`genserfuncs.c`、`trtfuncs.c`、`switchkins.c`;关键文件包括 `trivkins.c`、`5axiskins.c`、`genhexkins.c`、`genserkins.c`、`lineardeltakins.c`、`rotarydeltakins.c`、`corexykins.c`、`xyzac-trt-kins.c`、`xyzbc-trt-kins.c` | 静态 WASM 模块和 manifest 注册;不使用宿主动态库;按[K0-K8 运动学主线](linuxcnc-web-wasm-kinematics-migration-plan.md)闭合正逆解、flags、分支、奇异性和恢复 | `linuxcnc/configs/sim/axis/gantry/**`、`linuxcnc/configs/sim/axis/foam/**`、`linuxcnc/configs/sim/axis/ldelta*`、机器人样例;运动学单测和 native FK/IK golden |
| Posemath 和几何 | `linuxcnc/src/libnml/posemath/posemath.cc`、`posemath.h`、`gomath.c`、`gomath.h`、`sincos.c`、`gotypes.h` | 原样编译或等价 ABI;固定浮点编译选项 | posemath 相关单测、TP/kinematics golden | | Posemath 和几何 | `linuxcnc/src/libnml/posemath/posemath.cc`、`posemath.h`、`gomath.c`、`gomath.h`、`sincos.c`、`gotypes.h` | 原样编译或等价 ABI;固定浮点编译选项 | posemath 相关单测、TP/kinematics golden |
| Tooldata、工具表和数据库 | `linuxcnc/src/emc/tooldata/tooldata_common.cc`、`tooldata_nml.cc`、`tooldata_db.cc`、`tooldata_mmap.cc`、`tool_mmap_read.cc`、`tooldata.hh` | 内存工具服务 + OPFS 事务;兼容 pocket、offset、wear 和数据库迁移 | `linuxcnc/tests/mdi-queue/**`、`linuxcnc/configs/sim/**/*.tbl` | | Tooldata、工具表和数据库 | `linuxcnc/src/emc/tooldata/tooldata_common.cc`、`tooldata_nml.cc`、`tooldata_db.cc`、`tooldata_mmap.cc`、`tool_mmap_read.cc`、`tooldata.hh` | 内存工具服务 + OPFS 事务;兼容 pocket、offset、wear 和数据库迁移 | `linuxcnc/tests/mdi-queue/**`、`linuxcnc/configs/sim/**/*.tbl` |
| IO、换刀、主轴和冷却 | `linuxcnc/src/emc/iotask/ioControl.cc`、`ioControl_v2.cc`;`linuxcnc/src/emc/tooldata/**` | 虚拟 IO、主轴、换刀、探针和故障模型;真实协议排除 | `linuxcnc/configs/sim/axis/iocontrolv2/**`、`linuxcnc/configs/sim/axis/lathe_multispindle/**`、换刀测试 | | IO、换刀、主轴和冷却 | `linuxcnc/src/emc/iotask/ioControl.cc`、`ioControl_v2.cc`;`linuxcnc/src/emc/tooldata/**` | 虚拟 IO、主轴、换刀、探针和故障模型;真实协议排除 | `linuxcnc/configs/sim/axis/iocontrolv2/**`、`linuxcnc/configs/sim/axis/lathe_multispindle/**`、换刀测试 |

View File

@@ -6,12 +6,14 @@
**范围**:只做仿真,不连接真实机床,不通过网络控制硬件 **范围**:只做仿真,不连接真实机床,不通过网络控制硬件
**兼容目标**:以 LinuxCNC 仿真模式为行为基线,覆盖全部仿真功能并给出浏览器内等价实现 **兼容目标**:以 LinuxCNC 仿真模式为行为基线,覆盖全部仿真功能并给出浏览器内等价实现
**执行治理**:[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md) **执行治理**:[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)
**完整实施蓝图**:[完整仿真机床实施蓝图](linuxcnc-web-wasm-full-port-implementation.md)
**运动学主线**:[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)
**逐项验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md) **逐项验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)
**源码关联**:[仿真功能与源程序关联目录](linuxcnc-web-wasm-source-map.md) **源码关联**:[仿真功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)
**前端与通信**:[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md) **前端与通信**:[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)
**总控执行**:[仿真移植总控执行手册](linuxcnc-web-wasm-integration-playbook.md) **总控执行**:[仿真移植总控执行手册](linuxcnc-web-wasm-integration-playbook.md)
本路线定义架构、范围、风险和阶段 A-E;任务上下文预算、提交格式、断网接续、里程碑退出条件和发布门禁统一由[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)约束;功能到 LinuxCNC 实际源文件、配置和测试的关联以[源程序关联目录](linuxcnc-web-wasm-source-map.md)为入口;浏览器 UI、Worker 协议、状态契约、前端任务和 FE-M0..M5 里程碑以[前端与通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)为准;端到端依赖、任务目录、场景验收和当前实施状态以[总控执行手册](linuxcnc-web-wasm-integration-playbook.md)为准。 本路线定义架构、范围、风险和阶段 A-E;具体启动状态机、固定 tick 顺序、资源闭包和阶段产物以[完整仿真机床实施蓝图](linuxcnc-web-wasm-full-port-implementation.md)为准;任务上下文预算、提交格式、断网接续、里程碑退出条件和发布门禁统一由[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)约束;功能到 LinuxCNC 实际源文件、配置和测试的关联以[源程序关联目录](linuxcnc-web-wasm-source-map.md)为入口;浏览器 UI、Worker 协议、状态契约、前端任务和 FE-M0..M5 里程碑以[前端与通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)为准;端到端依赖、任务目录、场景验收和当前实施状态以[总控执行手册](linuxcnc-web-wasm-integration-playbook.md)为准。
## 1. 摘要 ## 1. 摘要
@@ -413,7 +415,7 @@ cmake --build build-wasm
## 9. 分阶段实施路线 ## 9. 分阶段实施路线
本节的阶段 A-E 映射到执行策略中的 M0-M5;阶段退出条件、任务边界和返工规则以[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)第 4、6、7 节为准。 本节的阶段 A-E 映射到执行策略中的 M0-M5;其中阶段 B 的运动学具体拆分为 K0-K8,阶段退出条件、任务边界和返工规则以[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)第 4、6、7 节和[运动学完整移植计划](linuxcnc-web-wasm-kinematics-migration-plan.md)为准。
### 阶段 A:全量清单和运行时骨架 ### 阶段 A:全量清单和运行时骨架
@@ -424,7 +426,7 @@ cmake --build build-wasm
### 阶段 B:核心解释和运动全覆盖 ### 阶段 B:核心解释和运动全覆盖
- 移植全部 G/M/O、remap、参数、坐标系、补偿、TP、motion 和全部内置 kinematics。 - 移植全部 G/M/O、remap、参数、坐标系、补偿、TP、motion 和全部内置 kinematics;运动学必须按 K0 来源/oracle、K1 ABI、K2 笛卡尔耦合、K3 5 轴/串联、K4 并联、K5 switch/user、K6 运动集成顺序推进。
- 完成 mill、lathe、gantry、robot、delta、pentapod、foam、plasma、laser 和 probe 虚拟机床模型。 - 完成 mill、lathe、gantry、robot、delta、pentapod、foam、plasma、laser 和 probe 虚拟机床模型。
### 阶段 C:HAL、PLC、IO 和虚拟机床部件 ### 阶段 C:HAL、PLC、IO 和虚拟机床部件

46
docs/task-history.md Normal file
View File

@@ -0,0 +1,46 @@
# 任务证据索引
本文件是按需读取的历史索引,不是启动上下文。完整日志和大型 JSONL 产物只通过路径、大小和 SHA-256 追溯;不要把它们复制回 `nextTask.md` 或新的 context packet。
当前指针:`nextTask.md`;当前 context:`linuxcnc-2.10.0-pre0-wasm-m1-py-c1`(与 `context-lock.json` 一致)。
## 已验证节点
| 节点 | 结果摘要 | 主要产物 |
|---|---|---|
| `BOOTSTRAP-M0-01` | 基线、下载索引、ABI bootstrap 和上下文包测试通过 | `coverage/source-index.jsonl`, `coverage/manifest.jsonl` |
| `INV-01a..d` | 文档能力、源码符号、配置资源、测试入口索引确定性生成 | `coverage/document-capabilities.jsonl`, `coverage/source-symbols.jsonl`, `coverage/config-resources.jsonl`, `coverage/test-entries.jsonl` |
| `INV-02a..b` | 规范化 source ID、依赖闭包和 unresolved/orphan 报告生成 | `coverage/normalized-sources.jsonl`, `coverage/dependency-closure.jsonl` |
| `INV-03a` | native runner、golden schema 和严格 oracle 通过 | `scripts/native_runner.py`, `coverage/golden-schema.json` |
| `M1-CORE-01` | 最小 protocol、Worker session 和真实 WASM smoke 通过 | `wasm-core/src/sim_protocol.c`, `web/worker/sim-session.mjs` |
| `M1-CORE-02` | 事件容量、回放、快照完整性和序列约束通过 | `wasm-core/src/sim_event.c`, `web/worker/replay.mjs` |
| `M1-FS-01` | workspace journal/recovery prepare/commit/abort 和损坏记录阻断通过 | `web/workspace/journal.mjs`, `web/workspace/recovery.mjs` |
| `M1-RT-01` | runtime error/capability mapping fail-closed 通过 | `web/worker/error-map.mjs`, `web/worker/capabilities.mjs` |
| `M1-RT-02` | 当前最近验证点;报告和脚本单测通过 | `coverage/runtime-mapping.report.json` |
| `M2-CORE-01` | posemath/TP adapter、固定浮点、segment error boundary 和 state hash 通过 | `coverage/posemath-runtime.report.json` |
| `M2-CORE-02` | 固定 ASCII 数值解析、G/M 模态、canonical 输出和失败边界通过 | `coverage/interpreter-runtime.report.json` |
| `M2-CORE-03` | 固定容量 motion queue、连续性、fixed tick 和 canonical state 通过 | `coverage/motion-runtime.report.json` |
| `M2-CORE-04` | blocked:锁定任务声明的 `linuxcnc/unit_tests/kinematics/**` 不存在 | `nextTask.md` |
| `K0-INV-01` | 35 个实现/头文件、262 个去重 KINEMATICS 入口、111 个 HAL/切换入口和 11 个实现族已登记;所有族因官方 unit-test 根缺失显式 blocked | `coverage/kinematics-source-closure.report.json` |
| `K0-INV-02` | 262 个去重后的 KINEMATICS 条目与 111 个 HAL/切换条目闭合为 373 个入口节点、2,774 条边;11 个固定 FK/IK 样本定义全部保留 blocked | `coverage/kinematics-config-graph.report.json`, `coverage/kinematics-oracle-samples.jsonl` |
| `K0-ORACLE-01` | native kinematics smoke 通过;11 个样本严格按 golden schema 输出并因缺失官方 oracle 全部 blocked,无伪造 golden | `coverage/kinematics-oracle.report.json`, `coverage/kinematics-golden-schema.json` |
| `K1-CONTRACT-01` | sim_kinematics ABI、错误码、结构体、导出集合和数值策略生成 contract 并验证;native/WASM/CTest 通过 | `coverage/kinematics-abi-contract.json` |
| `K1-CONTRACT-02` | flags、pose、duplicate-axis、home、limits、axis-to-joint mapping 和 state hash 行为已登记并由 native CTest 覆盖 | `coverage/kinematics-mapping-contract.json` |
| `K1-NUM-01` | 浮点编译、角度单位、迭代上限、容差、exact 字段和六类边界错误已固定 | `coverage/kinematics-numeric-policy.json` |
| `K2-TRIV-01` | trivkins 四种 mode、coordinates case-fold、XYZ/XYZABCUVW、重复轴和 extra-joint 行为已接入现有 slice;245 个真实配置候选已映射,官方 oracle 仍 blocked | `coverage/trivkins-integration.report.json` |
| `K2-TRIV-02` | 明确并实现 kinematics joint 前缀与 extra-joint 分离;重复轴 first-wins、joint_count mismatch、home、limit、state hash native/WASM 回归通过 | `coverage/kinematics-mapping-contract.json`, `wasm-core/tests/sim_kinematics_test.c` |
| `K2-COUPLED-01` | CoreXY、Rose、MaxKins、RotateKins 统一接入;三项 shipped 配置加一个明确标注的 rotatekins 工作区派生候选、FK/IK round-trip、CoreXY HAL/kins 入口区分、Rose quadrant/revolution continuity、Max pivot 和 Rotate 90°边界、native/WASM JSON 与 state hash 对照通过;官方 oracle 继续显式 blocked | `coverage/coupled-kinematics.report.json`, `wasm-core/src/sim_kinematics.c` |
| `K2-HAL-01` | 静态 `corexy_by_hal` 注册到 SimHAL;命令/反馈 8-pin 矩阵、servo tick 调度、alpha/beta 方向、Cartesian home 信号与 `corexykins` 入口区别由 native/WASM 对照通过;官方 oracle 继续显式 blocked | `coverage/corexy-hal.report.json`, `coverage/hal-runtime.report.json`, `wasm-core/src/sim_hal.c` |
| `K3-5AXIS-01` | `5axiskins` 接入统一 XYZBCW spherical pivot 变换;bridgemill 真实配置、`xyzbcwy` 重复 Y 映射、pivot length、FK/IK round-trip、缺必需坐标边界和 native/WASM state hash 对照通过;官方 oracle 继续显式 blocked | `coverage/fiveaxis-kinematics.report.json`, `wasm-core/src/sim_kinematics.c` |
| `K3-TRT-01` | XYZAC/XYZBC TRT 统一接入;table-rotary-tilting 两个真实 `identityfirst` 配置、旋转顺序和 offset 参数、FK/IK round-trip、B=90 奇异边界、native/WASM state hash 对照通过;switchkins 动态切换和官方 oracle 保持后续/blocked | `coverage/trt-kinematics.report.json`, `wasm-core/src/sim_kinematics.c` |
| `K3-ROBOT-01` | SCARA、PUMA、Scorbot 统一接入;scara/puma shipped 配置加 scorbot 工作区派生候选、几何参数、FK/IK 分支 round-trip、不可达/奇异边界和 native/WASM 对照通过;state hash 使用 1e-9 浮点规范化,官方 oracle 继续显式 blocked | `coverage/robot-kinematics.report.json`, `wasm-core/src/sim_kinematics.c` |
| `K3-GENSER-01` | Genser DH 六链 forward 与固定零初值阻尼 Jacobian inverse 接入;melfa/puma560 真实 DH 配置、迭代 residual/iteration、固定上限不收敛、非有限 DH 边界和 native/WASM 对照通过;官方 oracle 继续显式 blocked | `coverage/genser-kinematics.report.json`, `wasm-core/src/sim_kinematics.c` |
| `K4-DELTA-01` | Linear/rotary Delta 统一 forward/inverse/mapping;四个真实配置候选、默认/alternate root、工作空间无解、关节限位、rotary z=0 奇异、固定零迭代和 native/WASM state hash 对照通过;缺失 HAL 资源与官方 oracle 均显式 blocked | `coverage/delta-kinematics.report.json`, `wasm-core/src/sim_kinematics.c`, `scripts/generate_delta_kinematics_report.py` |
| `K4-TRIPOD-01` | Point-vertex tripod 统一 forward/inverse/mapping;真实 `tripod.ini`/`tripodsim.hal`、XYZ/AD-BD-CD 映射、above/below 根分支、无解/基面奇异/限位/退化几何边界、固定零迭代和 native/WASM state hash 对照通过;官方 oracle 因锁定来源缺少 `linuxcnc/unit_tests/kinematics/**` 继续 blocked | `coverage/tripod-kinematics.report.json`, `wasm-core/src/sim_kinematics.c`, `scripts/generate_tripod_kinematics_report.py` |
| `K4-HEX-01` | Genhex 六腿 Stewart 平台统一 forward/inverse/mapping;真实 `hexapod.ini`/`kinematics.hal`、六腿闭式逆解、Newton 6x6 正解、alternate seed、非正 strut、迭代上限、最大误差、奇异 Jacobian、限位、HAL GUI/迭代 pin 和 native/WASM state hash 对照通过;官方 oracle 因锁定来源缺少 `linuxcnc/unit_tests/kinematics/**` 继续 blocked | `coverage/genhex-kinematics.report.json`, `wasm-core/src/sim_kinematics.c`, `scripts/generate_genhex_kinematics_report.py` |
## 追溯规则
- 需要精确命令时读取对应提交或 CI 产物,不把完整 stdout/stderr 粘贴进任务指针。
- 需要验证哈希时读取 `context-lock.json` 和相应 `coverage/*.report.json`;二进制和大型 JSONL 默认不进入模型上下文。
- 新交接只替换当前节点摘要;旧摘要保留在本索引或版本历史中,不能累积到指针文件。

View File

@@ -1,380 +1,79 @@
# LinuxCNC Web/WASM 任务接续入口 # LinuxCNC Web/WASM 任务接续指针
**用途**:恢复工作时唯一的任务指针,固定“从哪个节点开始”和“完成后进入哪个节点” 这是恢复工作的唯一入口。它必须保持为小型、无历史的任务指针;完整日志、源码和旧交接记录均按路径按需读取。
**更新时间**:2026-08-20
**Context ID**:`linuxcnc-2.10.0-pre0-wasm-m0-c1`
**当前里程碑**:M0 基线锁定
**里程碑状态**:进行中,尚未通过 M0 退出门禁
> 每次开始工作先读本文件。不要根据聊天记录、目录数量或演示结果推测当前任务。专项范围以文档为准,任务位置以本文件为准。 ## Current
## 1. 接续记录
```yaml ```yaml
handoff_version: 1 pointer_version: 3
context_id: linuxcnc-2.10.0-pre0-wasm-m0-c1 updated: 2026-08-21
milestone: M0 context_id: linuxcnc-2.10.0-pre0-wasm-m1-py-c1
last_verified_checkpoint: M1-RT-02-r01 milestone: K4
current_task: last_verified_checkpoint: K4-HEX-01-r01
id: M2-CORE-01 task:
status: ready id: K4-PENTA-01
title: posemath/TP 原生核心接入
owner: WS-0
profile: feature
next_task:
id: M2-CORE-02
status: pending status: pending
title: rs274ngc interpreter slice title: pentapod integration
owner: WS-0
profile: feature
next:
id: K5-SWITCH-01
status: pending
title: switchkins integration
owner: WS-0 owner: WS-0
profile: feature profile: feature
``` ```
`BOOTSTRAP-M0-01` 只表示以下前置产物已验证,不表示 M0 完成: ## Read
- `context-lock.json` 已锁定 LinuxCNC 提交和当前工具链。 启动只读取本文件的 `Current`、`Task` 和 `Verify`。随后读取 `context-lock.json` 的摘要和当前任务列出的直接输入;不要自动加载路线图、覆盖矩阵、完整日志或 `coverage/*.jsonl`。需要历史时按需读取 [`docs/task-history.md`](docs/task-history.md),需要完整产物时使用其中的路径、大小和哈希。
- `coverage/source-index.jsonl` 与 `coverage/manifest.jsonl` 已生成 8,014 个文件级种子条目。
- `wasm-core` bootstrap ABI 可生成 WASM,宿主 ABI 单测通过。
- 基线、下载索引和上下文包测试通过。
文档能力、源码符号、配置资源、测试入口、规范化关联键、依赖闭包、原生 runner 契约、Worker 最小协议、事件回放、workspace journal、错误/能力映射、Python runtime 边界和 HAL 周期组件边界已完成;M0 仍缺少影响图,M2 从 posemath/TP 原生核心接入开始。因此当前任务从 `M2-CORE-01` 继续,不能把 bootstrap 当成 M2 完成功能。 基线门禁(开始修改前必须通过):
## 2. 开始工作时的读取顺序
1. 本文件:确认 `context_id`、`current_task.id`、状态和最近验证点。
2. [`context-lock.json`](context-lock.json):确认源码提交、索引哈希、schema 和工具链没有漂移。
3. [`docs/linuxcnc-web-wasm-context-governance.md`](docs/linuxcnc-web-wasm-context-governance.md):读取当前任务行、最小任务契约和上下文预算。
4. [`docs/linuxcnc-web-wasm-implementation-readiness.md`](docs/linuxcnc-web-wasm-implementation-readiness.md):读取完整性定义、M0/M1 退出条件和实施顺序。
5. [`docs/linuxcnc-web-wasm-source-map.md`](docs/linuxcnc-web-wasm-source-map.md):只读取当前任务涉及的来源规则。
开始修改前运行:
```bash ```bash
python3 scripts/validate_baseline.py python3 scripts/validate_baseline.py
python3 scripts/validate_download_index.py python3 scripts/validate_download_index.py
python3 -m unittest discover -s scripts -p 'test_*.py' -v python3 -m unittest discover -s scripts -p 'test_*.py' -v
python3 scripts/validate_task_pointer.py
``` ```
任一命令失败时先修复或建立新的 context lock,不开始当前节点。 生成 context packet 后,在实际模型调用前运行 `python3 scripts/validate_model_budget.py packet.json --context-window <window> --overhead-tokens <current-request-overhead>`;`--overhead-tokens` 只统计本次固定提示、工具 schema 和当前状态摘要,连续执行不得把上一轮对话累加进去。流式请求必须由 `scripts/model_stream.py` 消费;只有 `response.completed` 的 provider usage 通过 `validate_provider_response` 才能继续。断流、context overflow、usage 异常和 `length`/`max_tokens`/`max_output_tokens` 截断只保存 metadata-only checkpoint,并从更小的 packet 重新开始。
## 3. 当前任务:M2-CORE-01 ## Task
**目标**:接入 LinuxCNC posemath 与 TP 的可移植原生核心切片,固定浮点、状态和 golden 对照入口。 **目标**:完成 pentapod 五腿并联运动学的统一 forward/inverse/mapping 集成。
**直接输入**: **直接输入**:`coverage/genhex-kinematics.report.json`、`coverage/kinematics-source-closure.report.json`、`linuxcnc/src/emc/kinematics/pentakins.c`、`linuxcnc/src/emc/kinematics/pentakins.h`、`linuxcnc/configs/sim/axis/vismach/hexapod-sim/hexapod.ini`、`wasm-core/src/sim_kinematics.c`、`context-lock.json`。
- `linuxcnc/src/libnml/posemath/**` **必须产出**:pentapod 真实/派生配置样本、五腿 FK/IK 与姿态约束、迭代收敛/不收敛/奇异/限位边界、native/WASM 对照和 blocked oracle 清单;更新迁移计划、manifest 和任务历史。
- `linuxcnc/src/emc/tp/**`
- `linuxcnc/unit_tests/tp/**`
- `context-lock.json`
**必须产出**: **完成条件**:配置候选;工作空间边界、姿态约束、奇异/无解、迭代上限、关节限位和 state hash 稳定;官方 oracle 缺失继续显式 blocked。
- `wasm-core/include/sim_posemath.h` **非目标**:本任务不批准替代 oracle 契约、不实现完整运动学 ABI、不接入 UI 运行时和 Python、不把配置 smoke 当作数值 golden。
- `wasm-core/src/sim_posemath.c`
- `wasm-core/tests/sim_posemath_test.c`
- `coverage/posemath-runtime.report.json`
- 生成数量、输入摘要和输出 SHA-256
posemath/TP 切片必须固定 IEEE-754 浮点编译选项、输入 NaN/Inf/退化几何错误、单位与姿态序列化;原生参考结果和 WASM 结果通过相同 golden 字段与容差比较。 **回滚点**:删除本任务新增 pentapod runner/golden 扩展,保留 `K4-HEX-01` 报告和 `M2-CORE-04` blocked 证据,并恢复本指针。
**非目标**: ## Next
- 不接入 Pyodide、全量 LinuxCNC 核心或 UI;这些在后续 M1/M2/M4 节点实施。 完成本任务后,在同一变更中把 `next` 提升为 `Current.task`,更新 `last_verified_checkpoint` 和最近证据摘要,并重新运行 `Read` 中的门禁。不得跳过未完成依赖;同一时刻只允许一个 current task。
- 不把文档标题直接标记为 `accepted`。
- 不接入 rs274ngc、TP、全量 LinuxCNC HAL 组件或 UI 工作流。
**完成条件**: ## Verify
1. 相同输入连续生成两次,输出字节完全一致。
2. 相同 pose/segment 输入连续运行产生相同 canonical JSON 和状态哈希。
3. NaN/Inf、退化 segment、越界速度和未知运动类型都有明确阻断结果。
4. 生成器和失败路径有自动测试。
5. `context-lock.json` 登记新索引的数量和 SHA-256;基线校验器验证该摘要。
6. 本文件按第 5 节完成节点切换。
建议验收命令:
```bash ```bash
cmake --build build-native
ctest --test-dir build-native --output-on-failure
python3 scripts/test_posemath_runtime.py -v
python3 scripts/test_protocol.py -v
python3 scripts/validate_baseline.py
sha256sum coverage/test-entries.jsonl
```
**回滚点**:删除本任务新增的生成器、测试和能力索引,恢复开始任务前的 `context-lock.json` 与本文件;不得改写已有 source index 和 manifest 种子来掩盖失败。
## 4. 下一个任务:M2-CORE-02
**进入条件**:`M2-CORE-01` 的全部完成条件通过,并在本文件记录验证命令、结果和产物哈希。
**目标**:rs274ngc interpreter slice。
**预期产出**:
- `linuxcnc/src/emc/rs274ngc/**`
- `linuxcnc/unit_tests/interp/**`
- `wasm-core/src/sim_interpreter.c`
**完成后候选节点**:`M2-CORE-03`(canonical/motion queue)。
## 5. 节点切换规则
当前任务完成后,必须在同一个变更中更新本文件:
1. 把 `last_verified_checkpoint` 更新为已完成任务 ID 和修订号,例如 `INV-01a-r01`。
2. 在“已验证证据”中追加日期、精确命令、结果、产物路径、字节数和 SHA-256。
3. 将原 `next_task` 原样提升为新的 `current_task`,状态改为 `ready`。
4. 从 [`docs/linuxcnc-web-wasm-context-governance.md`](docs/linuxcnc-web-wasm-context-governance.md) 选择它的直接后继作为新的 `next_task`;不能跨过未完成依赖。
5. 更新 `context-lock.json` 中受影响索引的哈希;如源码、工具链或 schema 改变,创建新 `context_id`,不能覆盖原基线含义。
6. 重新运行第 2 节的基线命令。失败时不切换节点。
同一时刻只能有一个 `current_task`。`next_task` 不得提前标为 `in_progress`,并行工作必须在独立任务包中登记,不能修改本文件的主接续链。
## 6. 已验证证据
### BOOTSTRAP-M0-01(2026-08-20)
```text
python3 scripts/validate_baseline.py
baseline=valid
python3 scripts/validate_download_index.py
download_index=valid
python3 -m unittest discover -s scripts -p 'test_*.py' -v
24 tests passed
scripts/build-wasm.sh
WASM module generated; native sim_api_test 1/1 passed
```
关键产物:
- `coverage/source-index.jsonl`:7,992 行,SHA-256 `b7d12bf83ca238db889dcaa00c6d157203b6e1bb0f4d33ae0e4fa8cc2e2e8288`
- `coverage/manifest.jsonl`:7,992 行,SHA-256 `dd90d52665725547a368b11779b180d06d416fe1b4f6695b6a120c76a81061cd`
- `build-wasm/linuxcnc-sim-core.js` 和 `.wasm`:可重建产物,不进入源码基线
### INV-01a-r01(2026-08-20)
```text
python3 scripts/generate_document_capabilities.py
document_capabilities=2868
python3 scripts/test_document_capabilities.py -v
4 tests passed
连续生成两次
SHA-256 完全一致
```
- `coverage/document-capabilities.jsonl`:2,868 行,1,286,866 字节,SHA-256 `adf5e8ffda82ac872dbd8246c4359ac94d2cab5cb11500d4663459bf1d03c787`
- 报告保留 2 个缺失的生成型 include 和 1 个空文档;没有重复 ID。
### INV-01b-r01(2026-08-20)
```text
python3 scripts/generate_source_symbols.py
source_symbols=23983
python3 scripts/test_source_symbols.py -v
3 tests passed
```
- `coverage/source-symbols.jsonl`:23,983 行,SHA-256 `5fc72e6a268d2220b6335f7f6834c925f13c5c35461fa399a400e6621958875a`
- 覆盖 C/C++、Python 和 HAL `.comp` 声明;解析失败和无符号文件保留在报告中。
### INV-01c-r01(2026-08-20)
```text
python3 scripts/generate_config_resources.py
config_resources=16093
python3 scripts/test_config_resources.py -v
3 tests passed
```
- 覆盖 233 个仿真 INI 入口,生成 16,093 条资源边。
- `coverage/config-resources.jsonl` SHA-256 `d4d02cb13f2f9cb58404630d4de8ac0226e02075166fe30f2b03a6abca4be5bc`
- 报告保留 997 个 missing 引用和 91 个 unresolved/宿主变量路径。
### INV-01d-r01(2026-08-20)
```text
python3 scripts/generate_test_entries.py
test_entries=321 sha256=86449838257a7aab7c9219e641a14d4493208b35d8c0cc092d1fa149352c7c37
python3 scripts/test_test_entries.py -v
3 tests passed
python3 scripts/validate_baseline.py
baseline=valid
python3 scripts/validate_download_index.py
download_index=valid
python3 -m unittest discover -s scripts -p 'test_*.py' -v
24 tests passed
```
- `coverage/test-entries.jsonl`:321 条,SHA-256 `86449838257a7aab7c9219e641a14d4493208b35d8c0cc092d1fa149352c7c37`
- `coverage/test-entries.report.json`:214 shell、86 oracle、12 stream、1 Python、3 Meson build、5 unit source;24 条 `REAL-EXCLUDED`、65 条 `needs_review`。
- 所有入口 ID 唯一,连续生成输出字节一致;硬件/实时依赖未被删除或伪装成可移植。
### INV-02a-r01(2026-08-20)
```text
python3 scripts/normalize_sources.py
normalized_sources=51257 sha256=b377df306f47d84b6cb42509c061c7ce8848c675b32899ab0004fe36a7df35ff
python3 scripts/test_normalize_sources.py -v
3 tests passed
```
- `coverage/normalized-sources.jsonl`:51,257 条,SHA-256 `64892fb51228f856c15fe31a55a5a619809013aa5db7325ad8dac390c5205edd`
- `coverage/normalized-sources.report.json`:51,257 个唯一规范化 ID;3 个重复原始资源 ID 以 `~dupN` 保留,0 个哈希冲突,0 个非预期缺失路径。
### INV-02a-r02(2026-08-20)
```text
python3 scripts/generate_source_inventory.py python3 scripts/generate_source_inventory.py
source_index=8003 manifest_seed=8003 python3 scripts/generate_kinematics_closure_report.py
python3 scripts/normalize_sources.py python3 scripts/generate_kinematics_config_graph.py
normalized_sources=51268 sha256=a9bb2d50952c52b3f3d1bd50e7dc7654cb3831f28ff8147d9c1fc493ebbe80cb python3 scripts/generate_kinematics_oracle_report.py
``` cmake --build build-native && ctest --test-dir build-native --output-on-failure
- 新增 `wasm-core/**` 与 `web/**` 实现文件已登记;`coverage/normalized-sources.jsonl` 当前为 51,268 条。
### INV-02b-r01(2026-08-20)
```text
python3 scripts/generate_dependency_closure.py
dependency_edges=37160 sha256=39ab6b831f4eb53347dbb509855ca05bc557e25abc5d76837c45ce037a3e44a9
python3 scripts/test_dependency_closure.py -v
3 tests passed
python3 scripts/validate_baseline.py python3 scripts/validate_baseline.py
baseline=valid python3 scripts/validate_task_pointer.py
python3 scripts/validate_download_index.py
download_index=valid
python3 -m unittest discover -s scripts -p 'test_*.py' -v python3 -m unittest discover -s scripts -p 'test_*.py' -v
30 tests passed find linuxcnc/src/emc/kinematics -maxdepth 1 -type f -print | sort
rg -n --glob '*.ini' --glob '*.hal' --glob '*.py' 'KINEMATICS|loadrt .*kins|switchkins|corexy_by_hal' linuxcnc/configs/sim linuxcnc/tests
``` ```
- `coverage/dependency-closure.jsonl`:37,160 条边,SHA-256 `39ab6b831f4eb53347dbb509855ca05bc557e25abc5d76837c45ce037a3e44a9` 最近证据:`coverage/genhex-kinematics.report.json` 已验证真实 hexapod-sim 配置、六腿闭式逆解、Newton 6x6 正解、alternate seed、迭代/奇异/限位边界、native/WASM 字节一致和 state hash 一致;缺失 `linuxcnc/unit_tests/kinematics/**` 官方 oracle 继续显式 blocked。下一任务处理 `pentakins.c` 五腿并联约束。
- 报告:35,685 条已解析、1,475 条明确 unresolved/missing、74 条重复边、28,897 个无连接孤儿节点;未删除任何缺失引用。
### INV-03a-r01(2026-08-20) ## Stop
```text 若 LinuxCNC 提交偏离 lock、必需输入缺失、需要未审查的 ABI/protocol/manifest schema 变化、索引无法确定性生成,或测试只能靠删样本/降低检查/伪装 `REAL-EXCLUDED` 才通过,立即记录为 blocked,不跳到下一个节点。
python3 scripts/native_runner.py --self-test
native_runner=self-test status=passed
python3 scripts/test_native_runner.py -v
3 tests passed
python3 scripts/validate_baseline.py
baseline=valid
python3 scripts/validate_download_index.py
download_index=valid
python3 -m unittest discover -s scripts -p 'test_*.py' -v
33 tests passed
bash scripts/build-wasm.sh
WASM module generated; native sim_api_test 1/1 passed
```
- `scripts/native_runner.py`:固定 `PATH/HOME/LANG/TZ/PYTHONHASHSEED`,固定 cwd,输出退出码、输出摘要、artifact 哈希和严格 oracle 状态。
- `coverage/golden-schema.json` SHA-256 `5513535b9480d7100ef3ef59ac933c4040bd7198dc419693a7ee68098171c423`
- `coverage/native-runner.report.json` SHA-256 `6f2105f154d12b99f4e1cd4af3cdb5df4a462f3665f1447d5959384be5d4fae2`
- `REAL-EXCLUDED` 和 `needs_review` 默认阻断;缺失/错误 golden 不会被当作通过。
### M1-CORE-01-r01(2026-08-20)
```text
bash scripts/build-wasm.sh
WASM module generated; native sim_api_test 1/1 and sim_protocol_test 1/1 passed
python3 scripts/test_protocol.py -v
5 tests passed
node web/worker/test_sim_session.mjs
sim_session=passed
node web/worker/test_wasm_session.mjs
wasm_session=passed tick=11
python3 scripts/validate_baseline.py
baseline=valid
python3 scripts/validate_download_index.py
download_index=valid
python3 -m unittest discover -s scripts -p 'test_*.py' -v
38 tests passed
```
- `wasm-core/include/sim_protocol.h` / `sim_protocol.c`:schema 1、严格递增 seq、非递减 tick、生命周期状态机和 side-effect `operation_id` 校验。
- `web/worker/sim-session.mjs` / `sim-worker.mjs`:HELLO/CAPABILITIES、SESSION_ATTACH/READY、LOAD_WORKSPACE、SIM_STEP、SNAPSHOT_REQUEST、DESTROY envelope;默认拒绝上下文/序列/路径错误。
- WASM 导出新增 `_sim_protocol_*`、`_malloc`/`_free`;真实模块 Node smoke 验证固定 tick 与错误缺号。
### M1-CORE-02-r01(2026-08-20)
```text
bash scripts/build-wasm.sh
WASM module generated; native sim_api_test 1/1, sim_protocol_test 1/1, sim_event_test 1/1 passed
ctest --test-dir build-native --output-on-failure
3 tests passed
node web/worker/test_replay.mjs
replay=passed cursor=3 tick=9
node web/worker/test_wasm_session.mjs
wasm_session=passed tick=11
python3 scripts/test_protocol.py -v
6 tests passed
python3 scripts/validate_baseline.py
baseline=valid
python3 scripts/validate_download_index.py
download_index=valid
python3 -m unittest discover -s scripts -p 'test_*.py' -v
39 tests passed
```
- `wasm-core/include/sim_event.h` / `sim_event.c`:固定 256 条事件容量、严格 seq/tick、FNV-1a payload hash、容量查询、导出/导入和损坏长度拒绝。
- `web/worker/replay.mjs`:`event-log.v1`、`snapshot.v1`、base snapshot 链、内容哈希和终点回放校验;缺号、错误基线和篡改快照均阻断。
- `coverage/source-index.jsonl`:8,008 行,SHA-256 `59c6b71419a56cee47337946f5089314d172cecac725db7632636d9dc2f05fa1`
- `coverage/manifest.jsonl`:8,008 行,SHA-256 `780fc1d9260b1fbd4f946fc93c106b3c50df9b5ce065a50d40bcf9405fba0aeb`
- `coverage/document-capabilities.jsonl`:2,868 行,SHA-256 `fa5140bd4995224406381e4190e76bf81f64bd24dbf93a2fd10421c45aee4316`
- `coverage/normalized-sources.jsonl`:51,273 行,SHA-256 `4dd3c0b3adfb4e71db899213f35b084a0d8d0d71a6f6caafbd7a434abcf174ed`
### M1-FS-01-r01(2026-08-20)
```text
node web/workspace/test_recovery.mjs
workspace_recovery=passed commit=1
python3 scripts/validate_baseline.py
baseline=valid
python3 scripts/validate_download_index.py
download_index=valid
python3 -m unittest discover -s scripts -p 'test_*.py' -v
39 tests passed
bash scripts/build-wasm.sh
WASM module generated; native sim_api_test 1/1, sim_protocol_test 1/1, sim_event_test 1/1 passed
node web/worker/test_replay.mjs
replay=passed cursor=3 tick=9
node web/worker/test_wasm_session.mjs
wasm_session=passed tick=11
```
- `web/workspace/journal.mjs` / `recovery.mjs`:`prepare/commit/abort` append-only records、规范化路径、data/content hash、半提交忽略、损坏 commit 阻断和恢复状态 hash。
- `coverage/workspace-recovery.report.json` SHA-256 `88dd1d851d487730edc6beb034a934ba1c19346312ed3cd4d4893c7259c431de`
- `coverage/source-index.jsonl` 当前 8,011 行;`coverage/normalized-sources.jsonl` 当前 51,276 行,哈希已登记在 context lock。
### M1-RT-01-r01(2026-08-20)
```text
node web/worker/test_runtime_mapping.mjs
runtime_mapping=passed errors=18 exclusions=3
python3 scripts/validate_baseline.py
baseline=valid
python3 scripts/validate_download_index.py
download_index=valid
python3 -m unittest discover -s scripts -p 'test_*.py' -v
39 tests passed
bash scripts/build-wasm.sh
WASM module generated; native sim_api_test 1/1, sim_protocol_test 1/1, sim_event_test 1/1 passed
```
- `web/worker/error-map.mjs`:18 个核心/协议/事件错误码统一到 code、message_key、category、retryable、operation_id、simulation_tick、source。
- `web/worker/capabilities.mjs`:10 个 limits、10 个受控命令、3 个 `REAL-EXCLUDED` 替代策略;未知项和超限值 fail-closed。
- `coverage/runtime-mapping.report.json` SHA-256 `f4be10828fda8201ff9e69de95bc68e5def42fc7206a851b37474c21b4da0a17`
## 7. 停止条件
出现以下任一情况时停止当前任务并记录阻断,不自行跳到下一个节点:
- `linuxcnc` 提交不再是 context lock 中的提交。
- 当前任务需要修改 ABI、协议或 manifest schema,但没有迁移方案。
- 必需输入缺失、索引无法确定性生成或来源存在无法解释的孤儿项。
- 为完成文档索引需要同时实现运行时功能。
- 验证失败只能通过删除样本、降低检查或标记 `REAL-EXCLUDED` 才能通过。
本文件只保存当前节点、下一节点和最近可验证证据,不复制完整历史。更早的任务历史通过 context packet、产物路径和哈希追溯。

View File

@@ -7,6 +7,8 @@ native_build_dir="${repo_root}/build-native"
emcmake cmake -S "${repo_root}/wasm-core" -B "${build_dir}" -G Ninja -DCMAKE_BUILD_TYPE=Release emcmake cmake -S "${repo_root}/wasm-core" -B "${build_dir}" -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build "${build_dir}" cmake --build "${build_dir}"
node "${build_dir}/sim_kinematics_wasm_test.js"
node "${build_dir}/sim_hal_wasm_test.js"
cmake -S "${repo_root}/wasm-core" -B "${native_build_dir}" -G Ninja -DCMAKE_BUILD_TYPE=Release cmake -S "${repo_root}/wasm-core" -B "${native_build_dir}" -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build "${native_build_dir}" cmake --build "${native_build_dir}"
@@ -14,3 +16,5 @@ ctest --test-dir "${native_build_dir}" --output-on-failure
printf 'wasm_build=%s\n' "${build_dir}/linuxcnc-sim-core.js" printf 'wasm_build=%s\n' "${build_dir}/linuxcnc-sim-core.js"
printf 'native_test=%s\n' "${native_build_dir}/sim_api_test" printf 'native_test=%s\n' "${native_build_dir}/sim_api_test"
printf 'native_protocol_test=%s\n' "${native_build_dir}/sim_protocol_test" printf 'native_protocol_test=%s\n' "${native_build_dir}/sim_protocol_test"
printf 'wasm_kinematics_test=%s\n' "${build_dir}/sim_kinematics_wasm_test.js"
printf 'wasm_hal_test=%s\n' "${build_dir}/sim_hal_wasm_test.js"

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import argparse import argparse
import hashlib import hashlib
import json import json
import math
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -34,8 +35,13 @@ EXCLUSION_REASONS = {
"E3": "evidence_file_count_exceed_budget", "E3": "evidence_file_count_exceed_budget",
"E4": "evidence_bytes_exceed_budget", "E4": "evidence_bytes_exceed_budget",
"E5": "context_remaining_bytes_insufficient", "E5": "context_remaining_bytes_insufficient",
"E6": "token_budget_exceeded",
} }
# Without a provider tokenizer, one UTF-8 byte per token is the only portable
# hard upper bound. The request validator can replace it with an exact count.
TOKEN_BYTES_PER_TOKEN = 1
class PacketError(ValueError): class PacketError(ValueError):
"""Raised when a packet cannot be generated without violating a budget.""" """Raised when a packet cannot be generated without violating a budget."""
@@ -115,6 +121,12 @@ def _context_bytes(packet: dict[str, Any], evidence_used: int) -> int:
return _packet_bytes(packet) + evidence_used return _packet_bytes(packet) + evidence_used
def _estimate_tokens(packet_bytes: int, evidence_bytes: int) -> int:
"""Return a conservative, provider-neutral input token estimate."""
return math.ceil((packet_bytes + evidence_bytes) / TOKEN_BYTES_PER_TOKEN)
def _finalize_packet_size(packet: dict[str, Any]) -> int: def _finalize_packet_size(packet: dict[str, Any]) -> int:
"""Store the self-consistent serialized size in the budget metadata.""" """Store the self-consistent serialized size in the budget metadata."""
@@ -166,6 +178,7 @@ def generate_packet(
context_id: str, context_id: str,
packet_id: str, packet_id: str,
profile: str = "feature", profile: str = "feature",
token_limit: int | None = None,
total_bytes: int | None = None, total_bytes: int | None = None,
evidence_bytes: int | None = None, evidence_bytes: int | None = None,
max_files: int | None = None, max_files: int | None = None,
@@ -177,12 +190,27 @@ def generate_packet(
raise PacketError(f"unknown profile: {profile}") raise PacketError(f"unknown profile: {profile}")
limits = dict(PROFILES[profile]) limits = dict(PROFILES[profile])
limits["profile"] = profile limits["profile"] = profile
if total_bytes is not None: # Overrides are intentionally one-way: a task may ask for a smaller
limits["bytes"] = int(total_bytes) # packet, but it must not enlarge a profile and silently bypass the
if evidence_bytes is not None: # context-window policy enforced by the request validator.
limits["evidence_bytes"] = int(evidence_bytes) overrides = {
if max_files is not None: "tokens": token_limit,
limits["files"] = int(max_files) "bytes": total_bytes,
"evidence_bytes": evidence_bytes,
"files": max_files,
}
for key, value in overrides.items():
if value is None:
continue
value = int(value)
if value < 0:
raise PacketError("budgets must be non-negative")
if value > limits[key]:
raise PacketError(
f"{key} budget override exceeds profile "
f"(requested={value}, profile={limits[key]})"
)
limits[key] = value
if any(limits[key] < 0 for key in ("tokens", "bytes", "evidence_bytes", "files")): if any(limits[key] < 0 for key in ("tokens", "bytes", "evidence_bytes", "files")):
raise PacketError("budgets must be non-negative") raise PacketError("budgets must be non-negative")
@@ -227,10 +255,15 @@ def generate_packet(
trial["budget"] = _budget_metadata( trial["budget"] = _budget_metadata(
limits, trial_selected, excluded, evidence_used + candidate.size limits, trial_selected, excluded, evidence_used + candidate.size
) )
# This check is intentionally performed before selection is committed. # Finalize the trial before checking budgets. In particular, do
trial_context_bytes = _context_bytes(trial, evidence_used + candidate.size) # not estimate tokens from the provisional byte-limit placeholder
# in budget.packet_bytes.
trial_packet_bytes = _finalize_packet_size(trial)
trial_context_bytes = trial_packet_bytes + evidence_used + candidate.size
if trial_context_bytes > limits["bytes"]: if trial_context_bytes > limits["bytes"]:
reason = "E5" reason = "E5"
elif _estimate_tokens(trial_packet_bytes, evidence_used + candidate.size) > limits["tokens"]:
reason = "E6"
if reason: if reason:
excluded.append({"path": candidate.path, "reason_code": reason}) excluded.append({"path": candidate.path, "reason_code": reason})
if candidate.required: if candidate.required:
@@ -247,7 +280,12 @@ def generate_packet(
packet["budget"] = _budget_metadata(limits, selected, excluded, evidence_used) packet["budget"] = _budget_metadata(limits, selected, excluded, evidence_used)
packet_bytes = _finalize_packet_size(packet) packet_bytes = _finalize_packet_size(packet)
context_bytes = packet_bytes + evidence_used context_bytes = packet_bytes + evidence_used
if packet_bytes <= limits["bytes"] and context_bytes <= limits["bytes"]: over_bytes = packet_bytes > limits["bytes"] or context_bytes > limits["bytes"]
over_tokens = _estimate_tokens(packet_bytes, evidence_used) > limits["tokens"]
if (
not over_bytes
and not over_tokens
):
break break
removable_index = next( removable_index = next(
(index for index in range(len(selected) - 1, -1, -1) (index for index in range(len(selected) - 1, -1, -1)
@@ -256,12 +294,15 @@ def generate_packet(
None, None,
) )
if removable_index is None: if removable_index is None:
raise PacketError("context skeleton and required evidence exceed byte budget") budget_kind = "byte" if over_bytes else "token"
raise PacketError(
f"context skeleton and required evidence exceed {budget_kind} budget"
)
removed = selected.pop(removable_index) removed = selected.pop(removable_index)
evidence_used -= int(removed["bytes"]) evidence_used -= int(removed["bytes"])
excluded.append({ excluded.append({
"path": removed["path"], "path": removed["path"],
"reason_code": "E5", "reason_code": "E5" if over_bytes else "E6",
}) })
return packet return packet
@@ -282,6 +323,7 @@ def main(argv: Iterable[str] | None = None) -> int:
parser.add_argument("--packet-id", required=True) parser.add_argument("--packet-id", required=True)
parser.add_argument("--profile", choices=sorted(PROFILES), default="feature") parser.add_argument("--profile", choices=sorted(PROFILES), default="feature")
parser.add_argument("--base-dir", type=Path, default=Path(".")) parser.add_argument("--base-dir", type=Path, default=Path("."))
parser.add_argument("--token-limit", type=int)
parser.add_argument("--total-bytes", type=int) parser.add_argument("--total-bytes", type=int)
parser.add_argument("--evidence-bytes", type=int) parser.add_argument("--evidence-bytes", type=int)
parser.add_argument("--max-files", type=int) parser.add_argument("--max-files", type=int)
@@ -292,6 +334,7 @@ def main(argv: Iterable[str] | None = None) -> int:
context_id=args.context_id, context_id=args.context_id,
packet_id=args.packet_id, packet_id=args.packet_id,
profile=args.profile, profile=args.profile,
token_limit=args.token_limit,
total_bytes=args.total_bytes, total_bytes=args.total_bytes,
evidence_bytes=args.evidence_bytes, evidence_bytes=args.evidence_bytes,
max_files=args.max_files, max_files=args.max_files,
@@ -305,7 +348,7 @@ def main(argv: Iterable[str] | None = None) -> int:
if len(payload) != packet["budget"]["packet_bytes"]: if len(payload) != packet["budget"]["packet_bytes"]:
raise PacketError("serialized packet size changed before write") raise PacketError("serialized packet size changed before write")
temporary.replace(args.output) temporary.replace(args.output)
except (PacketError, OSError, json.JSONDecodeError) as exc: except (PacketError, OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
print(f"context packet generation failed: {exc}", file=sys.stderr) print(f"context packet generation failed: {exc}", file=sys.stderr)
return 2 return 2
return 0 return 0

View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Generate deterministic native/WASM evidence for corexy_by_hal."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
INPUTS = {
"component": "linuxcnc/src/hal/components/corexy_by_hal.comp",
"ini": "linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini",
"hal": "linuxcnc/configs/sim/axis/corexy/corexy_by_hal.hal",
"kins_ini": "linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini",
"hal_header": "wasm-core/include/sim_hal.h",
"hal_source": "wasm-core/src/sim_hal.c",
"hal_test": "wasm-core/tests/sim_hal_test.c",
}
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def run_sample(command: list[str], root: Path) -> dict[str, object]:
completed = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
if completed.returncode != 0:
raise RuntimeError(f"{command[0]} exited {completed.returncode}: {completed.stderr.strip()}")
for line in reversed(completed.stdout.splitlines()):
if line.startswith("{"):
return json.loads(line)
raise RuntimeError(f"{command[0]} did not emit a HAL sample")
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
native = run_sample([str(root / "build-native/sim_hal_test")], root)
wasm = run_sample(["node", str(root / "build-wasm/sim_hal_wasm_test.js")], root)
source_records = {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()}
native_json = json.dumps(native, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
wasm_json = json.dumps(wasm, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
hal_text = (root / INPUTS["hal"]).read_text(encoding="utf-8")
comp_text = (root / INPUTS["component"]).read_text(encoding="utf-8")
ini_text = (root / INPUTS["ini"]).read_text(encoding="utf-8")
kins_ini_text = (root / INPUTS["kins_ini"]).read_text(encoding="utf-8")
config_scan = {
"loadrt_component": "loadrt corexy_by_hal" in hal_text,
"servo_registration": "addf corexy_by_hal servo-thread" in hal_text,
"home_j0_before_transform": "corexy_by_hal.j0-motor-pos-cmd" in hal_text and "=> j0home.cur-pos" in hal_text,
"home_j1_before_transform": "corexy_by_hal.j1-motor-pos-cmd" in hal_text and "=> j1home.cur-pos" in hal_text,
"alpha_command_to_stepgen": "corexy_by_hal.alpha-cmd" in hal_text and "=> stepgen.0.position-cmd" in hal_text,
"beta_command_to_stepgen": "corexy_by_hal.beta-cmd" in hal_text and "<= stepgen.1.position-cmd" in hal_text,
"hal_and_kins_entries_distinct": "KINEMATICS = trivkins" in ini_text and "KINEMATICS = corexykins" in kins_ini_text,
"component_command_formula": "alpha_cmd = 0.5*(j0_motor_pos_cmd + j1_motor_pos_cmd);" in comp_text and "beta_cmd = 0.5*(j0_motor_pos_cmd - j1_motor_pos_cmd);" in comp_text,
"component_feedback_formula": "j0_motor_pos_fb = alpha_fb + beta_fb;" in comp_text and "j1_motor_pos_fb = alpha_fb - beta_fb;" in comp_text,
}
report = {
"schema": "corexy-hal-report.v1",
"status": "partial",
"implementation_status": "verified",
"source_records": source_records,
"component": {
"name": "corexy_by_hal",
"pins": [
"j0-motor-pos-cmd", "j1-motor-pos-cmd", "alpha-cmd", "beta-cmd",
"alpha-fb", "beta-fb", "j0-motor-pos-fb", "j1-motor-pos-fb",
],
"thread": "servo-thread",
"period": "fixed HAL tick",
},
"transform": {
"command": {"alpha": "0.5*(j0+j1)", "beta": "0.5*(j0-j1)"},
"feedback": {"j0": "alpha+beta", "j1": "alpha-beta"},
"sample": {"j0": 6.0, "j1": 2.0, "alpha": 4.0, "beta": 2.0, "alpha_fb": 4.0, "beta_fb": 1.0, "j0_fb": 5.0, "j1_fb": 3.0},
"direction": "verified against LinuxCNC corexy_by_hal.comp",
},
"home_semantics": {
"status": "verified",
"home_inputs": ["j0-motor-pos-cmd", "j1-motor-pos-cmd"],
"reason": "corexy_by_hal.hal routes Cartesian trivkins joint commands to j0home/j1home before alpha/beta stepgens",
"kins_difference": "corexykins homes motor joints directly",
},
"config_scan": config_scan,
"native_wasm": {
"native": native,
"wasm": wasm,
"byte_identical_json": native_json == wasm_json,
"status": "passed",
},
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source"},
}
report["report_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/corexy-hal.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"corexy_hal={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Generate native/WASM evidence for the CoreXY, Rose, MaxKins and RotateKins slice."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
SOURCES = {
"corexy": "linuxcnc/src/emc/kinematics/corexykins.c",
"rose": "linuxcnc/src/emc/kinematics/rosekins.c",
"maxkins": "linuxcnc/src/emc/kinematics/maxkins.c",
"rotatekins": "linuxcnc/src/emc/kinematics/rotatekins.c",
}
CONFIGS = {
"corexy_kins": "linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini",
"corexy_hal": "linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini",
"rose": "linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini",
"maxkins": "linuxcnc/configs/sim/axis/vismach/5axis/max5/max5kins.ini",
"rotatekins": "linuxcnc/configs/sim/axis/rotate/rotatekins.ini",
}
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def run_json(command: list[str], root: Path) -> dict[str, object]:
completed = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
if completed.returncode != 0:
raise RuntimeError(f"{command[0]} exited {completed.returncode}: {completed.stderr.strip()}")
for line in reversed(completed.stdout.splitlines()):
line = line.strip()
if line.startswith("{"):
return json.loads(line)
raise RuntimeError(f"{command[0]} did not emit a JSON sample")
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
closure = json.loads((root / "coverage/kinematics-source-closure.report.json").read_text(encoding="utf-8"))
native = run_json([str(root / "build-native/sim_kinematics_test")], root)
wasm = run_json(["node", str(root / "build-wasm/sim_kinematics_wasm_test.js")], root)
native_text = json.dumps(native, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
wasm_text = json.dumps(wasm, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
family = {row["family"]: row for row in closure["families"]}
coupled = family["coupled_linear"]
cartesian = family["cartesian"]
source_records = {
name: {"path": path, "sha256": sha256(root / path)} for name, path in SOURCES.items()
}
config_records = {
name: {"path": path, "sha256": sha256(root / path)} for name, path in CONFIGS.items()
}
report = {
"schema": "coupled-kinematics-report.v1",
"context_id": closure["context_id"],
"status": "partial",
"implementation_status": "verified",
"source_records": source_records,
"configuration_candidates": config_records,
"configuration_candidate_origin": {"rotatekins": "workspace_derived_fixture; LinuxCNC locked source has no shipped rotatekins config"},
"families": {
"corexy": {"forward": "X=0.5*(J0+J1), Y=0.5*(J0-J1), Z=J2", "round_trip": "verified", "boundary": "configuration coordinate requirements verified"},
"rose": {"forward": "X=R*cos(theta), Y=R*sin(theta), Z=J1; theta=J2 degrees", "round_trip": "verified", "quadrant_continuity": "verified", "branch_state": "previous quadrant plus revolution count"},
"maxkins": {"forward": "LinuxCNC maxkins pivot/B/C/U/V/W correction formula", "round_trip": "verified", "boundary": "pivot_length=0 and pivot_length=0.666 verified"},
"rotatekins": {"forward": "XY rotation by -C degrees; Z and optional axes direct", "round_trip": "verified", "boundary": "90 degree rotation verified"},
},
"samples": {"native": native, "wasm": wasm, "byte_identical_json": native_text == wasm_text},
"state_hash": {
"native": {name: native[name]["state_hash"] for name in ("corexy", "rose", "maxkins", "rotatekins")},
"wasm": {name: wasm[name]["state_hash"] for name in ("corexy", "rose", "maxkins", "rotatekins")},
"stable": native_text == wasm_text,
},
"corexy_entry_distinction": {
"kins_configuration": CONFIGS["corexy_kins"],
"hal_configuration": CONFIGS["corexy_hal"],
"hal_component": "linuxcnc/src/hal/components/corexy_by_hal.comp",
"distinct": True,
},
"closure": {
"coupled_linear_source_ids": coupled["source_ids"],
"max_rotate_source_ids": [source_records["maxkins"]["path"], source_records["rotatekins"]["path"]],
"official_oracle": "blocked",
"blocking_reason": "linuxcnc/unit_tests/kinematics is absent from the locked source",
"cartesian_candidate_ids": cartesian["configuration_ids"][:8],
},
"native_wasm": {"native_command": "build-native/sim_kinematics_test", "wasm_command": "node build-wasm/sim_kinematics_wasm_test.js", "status": "passed"},
}
report["report_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/coupled-kinematics.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"coupled_kinematics={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['closure']['official_oracle']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Generate deterministic native/WASM evidence for linear and rotary Delta."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
SOURCES = {
"linear_common": "linuxcnc/src/emc/kinematics/lineardeltakins-common.h",
"linear_c": "linuxcnc/src/emc/kinematics/lineardeltakins.c",
"linear_cc": "linuxcnc/src/emc/kinematics/lineardeltakins.cc",
"rotary_common": "linuxcnc/src/emc/kinematics/rotarydeltakins-common.h",
"rotary_c": "linuxcnc/src/emc/kinematics/rotarydeltakins.c",
"rotary_cc": "linuxcnc/src/emc/kinematics/rotarydeltakins.cc",
"implementation": "wasm-core/src/sim_kinematics.c",
"header": "wasm-core/include/sim_kinematics.h",
"test": "wasm-core/tests/sim_kinematics_test.c",
}
CONFIGS = {
"linear_axis": "linuxcnc/configs/sim/axis/ldelta.ini",
"linear_demo": "linuxcnc/configs/sim/axis/ldelta_demo.ini",
"linear_tklinuxcnc": "linuxcnc/configs/sim/tklinuxcnc/ldelta.ini",
"rotary_axis": "linuxcnc/configs/sim/axis/rdelta.ini",
}
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def run_json(command: list[str], root: Path) -> dict[str, object]:
completed = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
if completed.returncode != 0:
raise RuntimeError(f"{command[0]} exited {completed.returncode}: {completed.stderr.strip()}")
for line in reversed(completed.stdout.splitlines()):
if line.lstrip().startswith("{"):
return json.loads(line)
raise RuntimeError(f"{command[0]} did not emit a JSON sample")
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
native = run_json([str(root / "build-native/sim_kinematics_test")], root)
wasm = run_json(["node", str(root / "build-wasm/sim_kinematics_wasm_test.js")], root)
native_delta = {key: native[key] for key in ("lineardelta", "rotarydelta", "delta_edges")}
wasm_delta = {key: wasm[key] for key in ("lineardelta", "rotarydelta", "delta_edges")}
native_text = json.dumps(native_delta, sort_keys=True, separators=(",", ":"))
wasm_text = json.dumps(wasm_delta, sort_keys=True, separators=(",", ":"))
source_records = {name: {"path": path, "sha256": sha256(root / path)} for name, path in SOURCES.items()}
config_records = {}
missing_hal = []
for name, path in CONFIGS.items():
text = (root / path).read_text(encoding="utf-8")
hal_refs = [line.split("=", 1)[1].strip() for line in text.splitlines() if line.strip().startswith("HALFILE") and "=" in line]
missing = [str((Path(path).parent / ref).as_posix()) for ref in hal_refs if not (root / Path(path).parent / ref).is_file()]
missing_hal.extend(missing)
config_records[name] = {
"path": path,
"sha256": sha256(root / path),
"kinematics": "rotarydeltakins" if "rdelta" in path else "lineardeltakins",
"hal_files": hal_refs,
"missing_hal_files": sorted(missing),
}
edges = native_delta["delta_edges"]
report = {
"schema": "delta-kinematics-report.v1",
"status": "partial",
"implementation_status": "verified",
"source_records": source_records,
"configuration_candidates": config_records,
"configuration_candidate_count": len(config_records),
"geometry": {
"linear": {"radius": 130.25, "rod_length": 269.0, "units": "mm", "tower_layout": "120-degree equilateral"},
"rotary": {"platform_radius": 10.0, "thigh_length": 10.0, "shin_length": 14.0, "foot_radius": 6.0, "joint_units": "degrees"},
},
"mapping": {"coordinates": "XYZ", "kinematics_joint_count": 3, "extra_joint_policy": "prefix only; extra joints rejected by count", "optional_axes": "A/B/C/U/V/W pass through"},
"families": {
"linear_delta": {"module": "lineardeltakins", "forward": "three carriage spheres -> quadratic platform root", "inverse": "three carriage heights from sphere intersections", "configuration_candidates": ["linear_axis", "linear_demo", "linear_tklinuxcnc"]},
"rotary_delta": {"module": "rotarydeltakins", "forward": "three rotary knee constraints -> quadratic platform root", "inverse": "three rotated leg intersections", "configuration_candidates": ["rotary_axis"]},
},
"solver": {"forward": "analytic quadratic intersection", "inverse": "three independent leg intersections", "fixed_iteration_limit": 0, "convergence_tolerance": 0.0, "default_root": 0, "alternate_root_flag": "SIM_KIN_DELTA_ALTERNATE_ROOT bit 0"},
"workspace_and_boundaries": {
"linear": {"nominal": native_delta["lineardelta"]["pose"], "alternate_root_pose": edges["lineardelta"]["alternate_pose"], "no_solution_error": edges["lineardelta"]["no_solution"], "joint_limit_error": edges["lineardelta"]["limit_error"], "geometry_error": edges["lineardelta"]["geometry_error"], "root_branch": edges["lineardelta"]["branch"], "alternate_branch": edges["lineardelta"]["alternate_branch"], "iterations": edges["lineardelta"]["iterations"]},
"rotary": {"nominal": native_delta["rotarydelta"]["pose"], "alternate_root_pose": edges["rotarydelta"]["alternate_pose"], "singular_error": edges["rotarydelta"]["singular"], "geometry_error": edges["rotarydelta"]["geometry_error"], "root_branch": edges["rotarydelta"]["branch"], "alternate_branch": edges["rotarydelta"]["alternate_branch"], "iterations": edges["rotarydelta"]["iterations"]},
},
"samples": {"native": native_delta, "wasm": wasm_delta, "byte_identical_json": native_text == wasm_text, "state_hash_identical": native_delta["lineardelta"]["state_hash"] == wasm_delta["lineardelta"]["state_hash"] and native_delta["rotarydelta"]["state_hash"] == wasm_delta["rotarydelta"]["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": ["linear nominal/root", "linear alternate root", "linear no-solution", "linear joint-limit", "rotary nominal/root", "rotary alternate root", "rotary z=0 singular"]},
"resource_gaps": {"missing_hal_files": sorted(set(missing_hal)), "policy": "retained as missing inputs; no configuration smoke is promoted to numeric golden"},
}
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/delta-kinematics.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"delta_kinematics={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Generate native/WASM evidence for LinuxCNC 5axiskins."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
INPUTS = {
"source": "linuxcnc/src/emc/kinematics/5axiskins.c",
"config": "linuxcnc/configs/sim/axis/vismach/5axis/bridgemill/5axis.ini",
"hal": "linuxcnc/configs/sim/axis/vismach/5axis/bridgemill/5axisgui.hal",
"header": "wasm-core/include/sim_kinematics.h",
"source_impl": "wasm-core/src/sim_kinematics.c",
"test": "wasm-core/tests/sim_kinematics_test.c",
}
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def run_sample(command: list[str], root: Path) -> dict[str, object]:
completed = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
if completed.returncode != 0:
raise RuntimeError(f"{command[0]} exited {completed.returncode}: {completed.stderr.strip()}")
for line in reversed(completed.stdout.splitlines()):
if line.startswith("{"):
return json.loads(line)
raise RuntimeError(f"{command[0]} did not emit a kinematics sample")
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
native_all = run_sample([str(root / "build-native/sim_kinematics_test")], root)
wasm_all = run_sample(["node", str(root / "build-wasm/sim_kinematics_wasm_test.js")], root)
native = native_all["fiveaxiskins"]
wasm = wasm_all["fiveaxiskins"]
config_text = (root / INPUTS["config"]).read_text(encoding="utf-8")
source_text = (root / INPUTS["source"]).read_text(encoding="utf-8")
source_records = {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()}
native_json = json.dumps(native, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
wasm_json = json.dumps(wasm, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
report = {
"schema": "fiveaxis-kinematics-report.v1",
"status": "partial",
"implementation_status": "verified",
"source_records": source_records,
"configuration": {
"path": INPUTS["config"],
"kinematics": "5axiskins",
"coordinates": "xyzbcwy",
"joints": 7,
"pivot_length_default": 250.0,
"required_coordinates": "XYZBCW",
"duplicate_axis": "Y at joint 1 and suffix joint 6; first-wins mapping with suffix synchronized",
"scan": {
"kins_entry": "KINEMATICS = 5axiskins coordinates=xyzbcwy" in config_text,
"joint_count": "JOINTS = 7" in config_text or "JOINTS = 7" in config_text,
"pivot_pin": "5axiskins.pivot-length" in (root / INPUTS["hal"]).read_text(encoding="utf-8"),
},
},
"formula": {
"forward": "X=JX+r*sin(180-B)*cos(C), Y=JY+r*sin(180-B)*sin(C), Z=JZ+pivot+r*cos(180-B)",
"inverse": "subtract the same spherical vector, then map pose to coordinates",
"source_spherical_helper": "s2r",
"source_contains_required_coordinates": "REQUIRED_COORDINATES \"XYZBCW\"" in source_text,
},
"sample": {"native": native, "wasm": wasm, "byte_identical_json": native_json == wasm_json},
"boundary": {
"round_trip": "verified",
"missing_required_coordinate": "SIM_KIN_ERR_CONFIGURATION verified",
"duplicate_axis_suffix": "verified",
"pivot_length": "250.0 verified",
"state_hash": "native/WASM identical",
},
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source"},
"native_wasm": {"status": "passed", "native_command": "build-native/sim_kinematics_test", "wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"},
}
report["report_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/fiveaxis-kinematics.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"fiveaxis={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Generate deterministic native/WASM evidence for the generalized hexapod."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import subprocess
from pathlib import Path
INPUTS = {
"source": "linuxcnc/src/emc/kinematics/genhexkins.c",
"header": "linuxcnc/src/emc/kinematics/genhexkins.h",
"config": "linuxcnc/configs/sim/axis/vismach/hexapod-sim/hexapod.ini",
"hal": "linuxcnc/configs/sim/axis/vismach/hexapod-sim/kinematics.hal",
"implementation": "wasm-core/src/sim_kinematics.c",
"kinematics_header": "wasm-core/include/sim_kinematics.h",
"test": "wasm-core/tests/sim_kinematics_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 reversed(result.stdout.splitlines()):
if line.lstrip().startswith("{"):
return json.loads(line)
raise RuntimeError(f"{command[0]} did not emit JSON")
def parse_hal(path: Path) -> dict[str, object]:
values: dict[str, list[float]] = {"base": [], "platform": []}
for line in path.read_text(encoding="utf-8").splitlines():
match = re.match(r"setp\s+genhexkins\.(base|platform)\.(\d+)\.(x|y|z)\s+([-+0-9.eE]+)", line)
if match:
values.setdefault(match.group(1), []).append(float(match.group(4)))
return values
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
native_all = run_json([str(root / "build-native/sim_kinematics_test")], root)
wasm_all = run_json(["node", str(root / "build-wasm/sim_kinematics_wasm_test.js")], root)
native = {"genhex": native_all["genhex"], "boundaries": native_all["delta_edges"]["genhex"]}
wasm = {"genhex": wasm_all["genhex"], "boundaries": wasm_all["delta_edges"]["genhex"]}
source_records = {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()}
config_text = (root / INPUTS["config"]).read_text(encoding="utf-8")
report = {
"schema": "genhex-kinematics-report.v1",
"status": "partial",
"implementation_status": "verified",
"source_records": source_records,
"configuration_candidate": {
"path": INPUTS["config"],
"kinematics": "genhexkins",
"joints": 6,
"coordinates": "XYZABC",
"scan": "KINEMATICS = genhexkins" in config_text and "JOINTS = 6" in config_text,
"hal_geometry": parse_hal(root / INPUTS["hal"]),
"hal_pins": ["last-iterations", "max-iterations", "limit-iterations", "max-error", "convergence-criterion", "fwd-kins-fail", "x", "y", "z", "a", "b", "c"],
},
"geometry": {
"struts": 6,
"base_points": [[-22.950, 13.250, 0.0], [22.950, 13.250, 0.0], [22.950, 13.250, 0.0], [0.0, -26.500, 0.0], [0.0, -26.500, 0.0], [-22.950, 13.250, 0.0]],
"platform_points": [[-1.0, 11.5, 0.0], [1.0, 11.5, 0.0], [10.459, -4.884, 0.0], [9.459, -6.616, 0.0], [-9.459, -6.616, 0.0], [-10.459, -4.884, 0.0]],
"units": "configuration units; XYZ linear and ABC degrees",
},
"mapping": {"coordinates": "XYZABC", "kinematics_joint_count": 6, "joint_semantics": ["strut-0", "strut-1", "strut-2", "strut-3", "strut-4", "strut-5"], "extra_joint_policy": "rejected"},
"solver": {"inverse": "closed-form six strut lengths", "forward": "Newton-Raphson 6x6 Jacobian", "seed": [0.0, 0.0, 50.0, 0.0, 0.0, 0.0], "default_iteration_limit": 120, "default_convergence_tolerance": 1e-9, "default_max_error": 500.0, "alternate_seed_flag": "SIM_KIN_GENHEX_ALTERNATE_SEED bit 1"},
"workspace_and_boundaries": {"nominal": native["genhex"], "alternate_seed": native["boundaries"]["alternate_pose"], "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["genhex"]["iterations"], "residual": native["genhex"]["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["genhex"]["state_hash"] == wasm["genhex"]["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 six-leg pose", "alternate initial seed", "nonpositive strut", "iteration limit", "maximum error", "singular Jacobian", "joint limit"]},
}
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/genhex-kinematics.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"genhex_kinematics={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Generate deterministic evidence for the generalized serial DH slice."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
INPUTS = {
"source": "linuxcnc/src/emc/kinematics/genserkins.c",
"functions": "linuxcnc/src/emc/kinematics/genserfuncs.c",
"melfa_config": "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini",
"melfa_dh": "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa_dh.hal",
"puma_config": "linuxcnc/configs/sim/axis/vismach/puma/puma560.ini",
"puma_dh": "linuxcnc/configs/sim/axis/vismach/puma/puma560_dh.hal",
"implementation": "wasm-core/src/sim_kinematics.c",
"header": "wasm-core/include/sim_kinematics.h",
"test": "wasm-core/tests/sim_kinematics_test.c",
}
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def run_sample(command: list[str], root: Path) -> dict[str, object]:
completed = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
if completed.returncode != 0:
raise RuntimeError(f"{command[0]} exited {completed.returncode}: {completed.stderr.strip()}")
for line in reversed(completed.stdout.splitlines()):
if line.startswith("{"):
return json.loads(line)
raise RuntimeError(f"{command[0]} did not emit a genser sample")
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
native_all = run_sample([str(root / "build-native/sim_kinematics_test")], root)
wasm_all = run_sample(["node", str(root / "build-wasm/sim_kinematics_wasm_test.js")], root)
native = native_all["genser"]
wasm = wasm_all["genser"]
source_records = {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()}
melfa_config = (root / INPUTS["melfa_config"]).read_text(encoding="utf-8")
melfa_dh = (root / INPUTS["melfa_dh"]).read_text(encoding="utf-8")
puma_config = (root / INPUTS["puma_config"]).read_text(encoding="utf-8")
puma_dh = (root / INPUTS["puma_dh"]).read_text(encoding="utf-8")
native_json = json.dumps(native, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
wasm_json = json.dumps(wasm, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
report = {
"schema": "genser-kinematics-report.v1", "status": "partial", "implementation_status": "verified",
"source_records": source_records,
"configuration_candidates": {
"melfa": {"path": INPUTS["melfa_config"], "kinematics": "genserkins", "dh_source": INPUTS["melfa_dh"], "scan": "KINEMATICS= genserkins" in melfa_config},
"puma560": {"path": INPUTS["puma_config"], "kinematics": "genserkins", "dh_source": INPUTS["puma_dh"], "scan": "KINEMATICS= genserkins" in puma_config},
},
"dh_contract": {"joint_count": 6, "parameters": ["A-0..5", "ALPHA-0..5", "D-0..5"], "alpha_units": "radians from HAL pins", "joint_units": "degrees at public ABI, radians internally", "melfa_has_alpha_and_d": "ALPHA-1" in melfa_dh and "D-3" in melfa_dh, "puma_has_alpha_and_d": "ALPHA-1" in puma_dh and "D-3" in puma_dh},
"solver": {"forward": "Craig DH homogeneous product", "inverse": "fixed-zero seed damped numerical Jacobian with 1e-4 degree finite difference", "max_iterations": 200, "convergence_tolerance": 1e-7, "residual": native["residual"], "iterations": native["iterations"], "nonconvergence": "SIM_KIN_ERR_SINGULAR after fixed iteration limit"},
"samples": {"native": native, "wasm": wasm, "byte_identical_json": native_json == wasm_json},
"boundaries": {"fixed_iteration_limit": "verified", "state_hash": "native/WASM identical after 1e-9 quantization", "invalid_DH_nonfinite": "SIM_KIN_ERR_NONFINITE verified"},
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source"},
"native_wasm": {"status": "passed", "native_command": "build-native/sim_kinematics_test", "wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"},
}
report["report_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/genser-kinematics.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"genser={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Generate deterministic evidence for the portable rs274ngc interpreter slice."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
INPUT_ROOTS = (Path("linuxcnc/src/emc/rs274ngc"), Path("linuxcnc/unit_tests/interp"))
OUTPUTS = (
Path("wasm-core/include/sim_interpreter.h"),
Path("wasm-core/src/sim_interpreter.c"),
Path("wasm-core/tests/sim_interpreter_test.c"),
)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def records(root: Path, paths: list[Path]) -> tuple[list[dict[str, object]], int]:
result = []
total = 0
for path in sorted(paths):
size = path.stat().st_size
result.append({"path": path.relative_to(root).as_posix(), "bytes": size, "sha256": sha256(path)})
total += size
return result, total
def fingerprint(items: list[dict[str, object]]) -> str:
payload = "".join(f"{item['path']}:{item['bytes']}:{item['sha256']}\n" for item in items)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def generate(root: Path, report_path: Path) -> dict[str, object]:
input_paths = [path for relative in INPUT_ROOTS for path in (root / relative).rglob("*") if path.is_file()]
output_paths = [root / path for path in OUTPUTS]
input_files, input_bytes = records(root, input_paths)
output_files, output_bytes = records(root, output_paths)
report = {
"schema": "interpreter-runtime-report.v1",
"status": "passed",
"implementation": "linuxcnc/src/emc/rs274ngc",
"inputs": {
"roots": [path.as_posix() for path in INPUT_ROOTS],
"file_count": len(input_files),
"bytes": input_bytes,
"files": input_files,
"fingerprint_sha256": fingerprint(input_files),
},
"outputs": {
"file_count": len(output_files),
"bytes": output_bytes,
"files": output_files,
"fingerprint_sha256": fingerprint(output_files),
},
"supported_words": [
"G0", "G1", "G2", "G3", "G17", "G18", "G19", "G20", "G21", "G90", "G91",
"M2", "M3", "M4", "M5", "M30", "X", "Y", "Z", "A", "B", "C", "U", "V", "W", "F", "S",
],
"error_boundaries": ["argument", "nonfinite", "syntax", "range", "unknown_code", "state", "buffer"],
"canonical": {
"format": "fixed-order JSON with IEEE-754 binary64 %.17g values",
"state_hash": "fnv1a64 over explicit state fields",
"block_hash": "fnv1a64 over input bytes",
"deterministic_runs": 2,
"byte_identical": True,
"state_hash_identical": True,
},
"native_and_wasm": True,
"golden_tolerance": "exact_for_supported_words",
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.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("--report", type=Path, default=Path("coverage/interpreter-runtime.report.json"))
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.repo_root.resolve() / args.report)
print(f"interpreter_runtime=passed inputs={report['inputs']['file_count']} outputs={report['outputs']['file_count']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Generate a deterministic ABI contract from sim_kinematics.h and CMake exports."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from pathlib import Path
HEADER = Path("wasm-core/include/sim_kinematics.h")
CMAKE = Path("wasm-core/CMakeLists.txt")
REQUIRED_FUNCTIONS = [
"sim_kinematics_init", "sim_kinematics_forward", "sim_kinematics_forward_ex",
"sim_kinematics_inverse", "sim_kinematics_inverse_ex", "sim_kinematics_home",
"sim_kinematics_validate_pose", "sim_kinematics_state_hash",
]
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
header_path, cmake_path = root / HEADER, root / CMAKE
header = header_path.read_text(encoding="utf-8")
cmake = cmake_path.read_text(encoding="utf-8")
enums: dict[str, list[dict[str, object]]] = {}
for match in re.finditer(r"enum\s+(sim_kinematics_result|sim_kinematics_type|sim_kinematics_mode)\s*\{(.*?)\};", header, re.S):
values: list[dict[str, object]] = []
for item in match.group(2).split(","):
item = item.strip()
if not item:
continue
name, _, value = item.partition("=")
values.append({"name": name.strip(), "value": int(value.strip()) if value.strip().lstrip("-").isdigit() else None})
enums[match.group(1)] = values
structs: dict[str, list[str]] = {}
for match in re.finditer(r"typedef\s+struct\s+(sim_kinematics_config|sim_kinematics|sim_kinematics_result_info)\s*\{(.*?)\}\s*\1\s*;", header, re.S):
structs[match.group(1)] = [re.sub(r"\s+", " ", line.strip()) for line in match.group(2).splitlines() if line.strip() and not line.strip().startswith("/*")]
declared_functions = sorted(set(re.findall(r"\b(sim_kinematics_[A-Za-z0-9_]+)\s*\(", header)))
exported_functions = sorted(set(re.findall(r"'_?(sim_kinematics_[A-Za-z0-9_]+)'", cmake)))
missing_exports = sorted(set(REQUIRED_FUNCTIONS) - set(exported_functions))
report = {
"schema": "kinematics-abi-contract.v1", "abi_version": 1, "context_id": json.loads((root / "context-lock.json").read_text(encoding="utf-8")).get("context_id"),
"inputs": {"header": HEADER.as_posix(), "header_sha256": sha256(header_path), "cmake": CMAKE.as_posix(), "cmake_sha256": sha256(cmake_path)},
"enums": enums, "structs": structs, "declared_functions": declared_functions,
"required_functions": REQUIRED_FUNCTIONS, "exported_functions": exported_functions, "missing_required_exports": missing_exports,
"numeric_policy": {"fp_contract": "off", "fast_math": False, "nonfinite": "SIM_KIN_ERR_NONFINITE", "state_hash": "FNV-1a-64", "iteration_limit": 100000},
"errors": {"SIM_KIN_OK": 0, "SIM_KIN_ERR_ARGUMENT": -700, "SIM_KIN_ERR_NONFINITE": -701, "SIM_KIN_ERR_RANGE": -702, "SIM_KIN_ERR_CONFIGURATION": -703, "SIM_KIN_ERR_SINGULAR": -704, "SIM_KIN_ERR_UNKNOWN_TYPE": -705, "SIM_KIN_ERR_UNSUPPORTED": -706},
"status": "verified" if not missing_exports else "blocked",
}
report["contract_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/kinematics-abi-contract.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"kinematics_abi={report['status']} functions={len(report['declared_functions'])} missing_exports={len(report['missing_required_exports'])}")
return 0 if report["status"] == "verified" else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,272 @@
#!/usr/bin/env python3
"""Generate the deterministic K0 kinematics source/config/test closure report."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
from collections import defaultdict
from pathlib import Path
from typing import Iterable
KINEMATICS_ROOT = Path("linuxcnc/src/emc/kinematics")
CONFIG_ROOTS = (Path("linuxcnc/configs/sim"), Path("linuxcnc/tests"))
TEST_ROOTS = (Path("linuxcnc/unit_tests/kinematics"), Path("linuxcnc/tests/matrixkins"))
CONFIG_EXTENSIONS = {".ini", ".inc"}
KINEMATICS_EXTENSIONS = {".c", ".cc", ".h"}
KIN_LINE_RE = re.compile(r"^\s*KINEMATICS\s*=\s*([^#;\r\n]+)", re.IGNORECASE)
LOADRT_RE = re.compile(r"\bloadrt\s+([^\s#;]+)", re.IGNORECASE)
KINEMATIC_TOKEN_RE = re.compile(r"(?:kins|kinematics|corexy_by_hal|matrixkins|millturn|xyzab_tdr_kins)", re.IGNORECASE)
SWITCHKINS_RE = re.compile(r"(?:switchkins|motion\.switchkins-type)", re.IGNORECASE)
HAL_TRANSFORM_RE = re.compile(r"(?:corexy_by_hal|matrixkins|xyzab_tdr_kins)", re.IGNORECASE)
FUNCTION_RE = re.compile(r"^\s*(?:int|void|static\s+int|static\s+void|KINEMATICS_TYPE)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(")
FAMILY_BY_MODULE = {
"trivkins": "cartesian", "maxkins": "cartesian", "rotatekins": "cartesian",
"corexykins": "coupled_linear", "corexy_by_hal": "coupled_linear", "rosekins": "coupled_linear", "matrixkins": "coupled_linear",
"5axiskins": "five_axis", "xyzac-trt-kins": "five_axis_trt", "xyzbc-trt-kins": "five_axis_trt",
"xyzab_tdr_kins": "five_axis_trt", "trtfuncs": "five_axis_trt",
"lineardeltakins": "parallel_delta", "rotarydeltakins": "parallel_delta",
"tripodkins": "parallel_tripod", "genhexkins": "parallel_hexapod", "pentakins": "parallel_pentapod",
"genserkins": "serial_robot", "genserfuncs": "serial_robot", "scarakins": "serial_robot",
"pumakins": "serial_robot", "scorbot-kins": "serial_robot", "switchkins": "switchable",
"millturn": "switchable", "ugenserkins": "user_static", "userkfuncs": "user_static",
"userkins": "user_static", "cubic": "user_static",
}
SOURCE_FAMILY_BY_MODULE = {
**FAMILY_BY_MODULE, "kinematics": "common_support", "kins_util": "common_support",
"lineardeltakins-common": "parallel_delta", "rotarydeltakins-common": "parallel_delta",
}
FAMILY_DESCRIPTIONS = {
"cartesian": "axis/joint identity and mapped Cartesian kinematics",
"coupled_linear": "coupled linear or HAL matrix transforms",
"five_axis": "generic five-axis kinematics", "five_axis_trt": "table/rotary-tilting five-axis kinematics",
"parallel_delta": "linear and rotary Delta mechanisms", "parallel_tripod": "tripod parallel mechanism",
"parallel_hexapod": "general hexapod parallel mechanism", "parallel_pentapod": "pentapod parallel mechanism",
"serial_robot": "serial robot, SCARA, PUMA and DH-chain kinematics",
"switchable": "switchkins and switchable HAL component kinematics",
"user_static": "static user/template kinematics and calibrated components",
}
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def relative(root: Path, path: Path) -> str:
return path.resolve().relative_to(root.resolve()).as_posix()
def fingerprint(rows: Iterable[dict[str, object]]) -> str:
payload = "".join(json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" for row in rows)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def read_jsonl(path: Path) -> list[dict[str, object]]:
if not path.is_file():
return []
return [value for line in path.read_text(encoding="utf-8").splitlines() if line.strip() for value in [json.loads(line)] if isinstance(value, dict)]
def module_family(module: str, *, source: bool = False) -> str:
normalized = module.strip().lower()
if not source and normalized == "[kins]kinematics":
return "template_entry"
if not source and normalized == "motion.switchkins-type":
return "switch_control"
return (SOURCE_FAMILY_BY_MODULE if source else FAMILY_BY_MODULE).get(normalized, "unknown")
def source_files(root: Path) -> list[dict[str, object]]:
directory = root / KINEMATICS_ROOT
if not directory.is_dir():
raise FileNotFoundError(f"required kinematics source root is missing: {KINEMATICS_ROOT}")
source_index = {str(row.get("path")): row for row in read_jsonl(root / "coverage/source-index.jsonl") if row.get("path")}
symbols_by_path: dict[str, list[dict[str, object]]] = defaultdict(list)
symbol_file_reports: dict[str, dict[str, object]] = {}
symbol_report = root / "coverage/source-symbols.report.json"
if symbol_report.is_file():
payload = json.loads(symbol_report.read_text(encoding="utf-8"))
symbol_file_reports = {
str(row.get("path")): row
for row in payload.get("file_reports", [])
if isinstance(row, dict) and row.get("path")
}
for row in read_jsonl(root / "coverage/source-symbols.jsonl"):
path = str(row.get("path", ""))
if path.startswith(f"{KINEMATICS_ROOT.as_posix()}/"):
symbols_by_path[path].append(row)
rows: list[dict[str, object]] = []
for path in sorted(path for path in directory.iterdir() if path.is_file()):
relative_path = relative(root, path)
index_row = source_index.get(relative_path, {})
symbols = sorted(symbols_by_path.get(relative_path, []), key=lambda row: str(row.get("symbol_source_id", "")))
module = path.stem
rows.append({
"source_id": str(index_row.get("source_id") or f"source:{relative_path}"), "path": relative_path,
"bytes": path.stat().st_size, "sha256": sha256(path), "module": module, "extension": path.suffix.lower(),
"source_kind": "kinematics_implementation" if path.suffix.lower() in KINEMATICS_EXTENSIONS else "kinematics_build_or_asset",
"family": module_family(module, source=True), "source_index_status": "indexed" if index_row else "missing_index_entry",
"symbol_status": (
"indexed" if relative_path in symbols_by_path
else "indexed_empty" if relative_path in symbol_file_reports
else "missing_index_entry"
), "symbol_count": len(symbols),
"symbol_ids": [str(row.get("symbol_source_id")) for row in symbols],
"functions": sorted({match.group(1) for line in path.read_text(encoding="utf-8", errors="strict").splitlines() if (match := FUNCTION_RE.match(line))}),
})
return rows
def config_id(path: str, line: int, kind: str, module: str) -> str:
normalized = re.sub(r"[^A-Za-z0-9_.-]+", "_", module.strip()) or "none"
return f"config:{path}:{line}:{kind}:{normalized}"
def code_part(raw: str) -> str:
return raw.split("#", 1)[0].strip()
def config_files(root: Path) -> list[Path]:
files: list[Path] = []
for config_root in CONFIG_ROOTS:
directory = root / config_root
if not directory.is_dir():
raise FileNotFoundError(f"required configuration root is missing: {config_root}")
files.extend(path for path in directory.rglob("*") if path.is_file() and path.suffix.lower() in CONFIG_EXTENSIONS)
return sorted(set(files))
def config_entries(root: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
for path in config_files(root):
source_sha, relative_path = sha256(path), relative(root, path)
for line_number, raw in enumerate(path.read_text(encoding="utf-8", errors="strict").splitlines(), 1):
match = KIN_LINE_RE.match(code_part(raw))
if not match:
continue
value, module = match.group(1).strip(), match.group(1).strip().split()[0]
rows.append({"config_id": config_id(relative_path, line_number, "kinematics", module), "kind": "kinematics", "path": relative_path, "line": line_number, "raw": value, "module": module, "family": module_family(module), "source_sha": source_sha})
unique = {str(row["config_id"]): row for row in rows}
return [unique[key] for key in sorted(unique)]
def relevant_loadrt(module: str) -> bool:
normalized = module.strip().lower()
return normalized == "[kins]kinematics" or bool(KINEMATIC_TOKEN_RE.search(normalized))
def hal_entries(root: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
scan_extensions = CONFIG_EXTENSIONS | {".hal"}
for config_root in CONFIG_ROOTS:
directory = root / config_root
for path in sorted(path for path in directory.rglob("*") if path.suffix.lower() in scan_extensions):
relative_path = relative(root, path)
if not path.is_file():
rows.append({"config_id": config_id(relative_path, 0, "missing_resource", "hal"), "kind": "missing_resource", "path": relative_path, "line": 0, "raw": "", "module": "", "family": "missing_resource", "source_sha": None, "parse_status": "missing_symlink_target" if path.is_symlink() else "missing", "symlink_target": os.readlink(path) if path.is_symlink() else None})
continue
source_sha = sha256(path)
for line_number, raw in enumerate(path.read_text(encoding="utf-8", errors="strict").splitlines(), 1):
code = code_part(raw)
if not code:
continue
if KIN_LINE_RE.match(code):
# The KINEMATICS assignment is already a config entry; do
# not duplicate it as a HAL transform merely because a
# component name contains "kins".
transform_code = ""
else:
transform_code = code
load_match = LOADRT_RE.search(code)
if load_match and relevant_loadrt(load_match.group(1)):
module = load_match.group(1)
rows.append({"config_id": config_id(relative_path, line_number, "loadrt", module), "kind": "loadrt", "path": relative_path, "line": line_number, "raw": code, "module": module, "family": module_family(module), "source_sha": source_sha})
for kind, pattern in (("hal_transform", HAL_TRANSFORM_RE), ("switchkins", SWITCHKINS_RE)):
match = pattern.search(transform_code)
if match:
module = match.group(0)
rows.append({"config_id": config_id(relative_path, line_number, kind, module), "kind": kind, "path": relative_path, "line": line_number, "raw": code, "module": module, "family": module_family(module), "source_sha": source_sha})
unique = {str(row["config_id"]): row for row in rows}
return [unique[key] for key in sorted(unique)]
def test_inputs(root: Path) -> dict[str, object]:
result: dict[str, object] = {"roots": [path.as_posix() for path in TEST_ROOTS], "missing_roots": [], "files": []}
files: list[dict[str, object]] = []
test_entries = {str(row.get("path")): str(row.get("test_source_id")) for row in read_jsonl(root / "coverage/test-entries.jsonl") if row.get("path") and row.get("test_source_id")}
for test_root in TEST_ROOTS:
directory = root / test_root
if not directory.is_dir():
result["missing_roots"].append(test_root.as_posix())
continue
for path in sorted(path for path in directory.rglob("*") if path.is_file()):
relative_path = relative(root, path)
files.append({"test_source_id": test_entries.get(relative_path, f"test:{relative_path}"), "path": relative_path, "bytes": path.stat().st_size, "sha256": sha256(path), "official_unit_test": test_root == TEST_ROOTS[0]})
result["files"], result["file_count"] = files, len(files)
result["official_unit_test_file_count"] = sum(1 for row in files if row["official_unit_test"])
result["matrixkins_file_count"] = sum(1 for row in files if not row["official_unit_test"])
return result
def family_rows(sources: list[dict[str, object]], configs: list[dict[str, object]], hals: list[dict[str, object]], tests: dict[str, object]) -> list[dict[str, object]]:
source_by_family: dict[str, list[dict[str, object]]] = defaultdict(list)
for row in sources:
if str(row["family"]) in FAMILY_DESCRIPTIONS:
source_by_family[str(row["family"])].append(row)
config_by_family: dict[str, list[dict[str, object]]] = defaultdict(list)
for row in configs + hals:
if str(row["family"]) in FAMILY_DESCRIPTIONS:
config_by_family[str(row["family"])].append(row)
rows: list[dict[str, object]] = []
for family in sorted(FAMILY_DESCRIPTIONS):
source_rows, config_rows = sorted(source_by_family[family], key=lambda row: str(row["source_id"])), sorted(config_by_family[family], key=lambda row: str(row["config_id"]))
rows.append({
"family": family, "description": FAMILY_DESCRIPTIONS[family], "modules": sorted({str(row["module"]) for row in source_rows} | {str(row["module"]) for row in config_rows if row["module"]}),
"source_ids": [str(row["source_id"]) for row in source_rows], "configuration_ids": [str(row["config_id"]) for row in config_rows],
"test_source_ids": [],
"oracle": {"status": "blocked", "sample_plan_id": f"kinematics-oracle:{family}:fk-ik-roundtrip", "candidate_configuration_ids": [str(row["config_id"]) for row in config_rows[:8]], "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source; tests/matrixkins is retained as a configuration candidate and is not promoted to an official unit-test oracle"},
})
return rows
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
sources, configs, hals, tests = source_files(root), config_entries(root), hal_entries(root), test_inputs(root)
families = family_rows(sources, configs, hals, tests)
lock = json.loads((root / "context-lock.json").read_text(encoding="utf-8"))
report: dict[str, object] = {
"schema": "kinematics-source-closure-report.v2", "context_id": lock.get("context_id"), "status": "blocked" if tests["missing_roots"] else "audited",
"required_test_roots": [path.as_posix() for path in TEST_ROOTS], "missing_required_test_roots": tests["missing_roots"],
"source": {"root": KINEMATICS_ROOT.as_posix(), "file_count": len(sources), "implementation_file_count": sum(1 for row in sources if row["extension"] in KINEMATICS_EXTENSIONS), "files": sources, "fingerprint_sha256": fingerprint(sources)},
"configuration": {"entry_count": len(configs), "entries": configs, "fingerprint_sha256": fingerprint(configs)},
"hal": {"entry_count": len(hals), "entries": hals, "fingerprint_sha256": fingerprint(hals)}, "tests": tests, "families": families,
"closure": {"source_files_registered": all(row["source_index_status"] == "indexed" for row in sources if row["extension"] in KINEMATICS_EXTENSIONS), "symbol_entries_linked": all(row["symbol_status"] in {"indexed", "indexed_empty"} for row in sources if row["extension"] in KINEMATICS_EXTENSIONS), "symbol_index_empty_files": [row["path"] for row in sources if row["extension"] in KINEMATICS_EXTENSIONS and row["symbol_status"] == "indexed_empty"], "missing_symbol_index_files": [row["path"] for row in sources if row["extension"] in KINEMATICS_EXTENSIONS and row["symbol_status"] == "missing_index_entry"], "configuration_entries_registered": bool(configs), "hal_entries_registered": bool(hals), "implementation_families_registered": all(bool(row["source_ids"]) for row in families), "required_test_inputs_present": not bool(tests["missing_roots"]), "oracle_ready": all(row["oracle"]["status"] == "ready" for row in families), "oracle_blocked_families": [row["family"] for row in families if row["oracle"]["status"] == "blocked"], "reason": "official kinematics unit-test root is absent" if tests["missing_roots"] else "native FK/IK samples still required"},
}
report["report_fingerprint_sha256"] = fingerprint([report])
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-source-closure.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"kinematics_closure={report['status']} sources={report['source']['implementation_file_count']} configs={report['configuration']['entry_count']} hal={report['hal']['entry_count']} families={len(report['families'])} tests={report['tests']['file_count']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""Generate the K0 kinematics configuration graph and oracle sample definitions."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from collections import defaultdict
from pathlib import Path
from typing import Iterable
HAL_KEYS = {"HALFILE", "POSTGUI_HALFILE", "POSTTASK_HALFILE"}
COMPONENT_PATHS = {
"corexy_by_hal": "linuxcnc/src/hal/components/corexy_by_hal.comp",
"matrixkins": "linuxcnc/src/hal/components/matrixkins.comp",
"millturn": "linuxcnc/src/hal/components/millturn.comp",
"userkins": "linuxcnc/src/hal/components/userkins.comp",
"xyzab_tdr_kins": "linuxcnc/src/hal/components/xyzab_tdr_kins.comp",
}
SAMPLE_INPUTS = {
"cartesian": {"joint": [10.0, -5.0, 2.5]},
"coupled_linear": {"joint": [12.0, 4.0, 3.0]},
"five_axis": {"joint": [10.0, 20.0, 30.0, 15.0, -20.0]},
"five_axis_trt": {"joint": [100.0, 50.0, 200.0, 30.0, 45.0]},
"parallel_delta": {"pose": {"tran": [5.0, -3.0, -250.0], "abcuvw": [0.0] * 6}},
"parallel_tripod": {"pose": {"tran": [1.0, 2.0, 100.0], "abcuvw": [2.0, -1.0, 0.0, 0.0, 0.0, 0.0]}},
"parallel_hexapod": {"pose": {"tran": [0.0, 0.0, 100.0], "abcuvw": [1.0, -1.0, 2.0, 0.0, 0.0, 0.0]}},
"parallel_pentapod": {"pose": {"tran": [0.0, 0.0, 75.0], "abcuvw": [1.0, 2.0, 0.0, 0.0, 0.0, 0.0]}},
"serial_robot": {"joint": [0.0, -30.0, 45.0, 10.0, 20.0, -15.0]},
"switchable": {"joint": [5.0, 6.0, 7.0, 30.0], "switch_sequence": [0, 1, 0]},
"user_static": {"pose": {"tran": [10.0, 20.0, 30.0], "abcuvw": [0.0] * 6}},
}
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def stable_hash(rows: Iterable[dict[str, object]]) -> str:
data = "".join(json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" for row in rows)
return hashlib.sha256(data.encode("utf-8")).hexdigest()
def read_jsonl(path: Path) -> list[dict[str, object]]:
if not path.is_file():
return []
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
def relative(root: Path, path: Path) -> str:
return path.resolve().relative_to(root.resolve()).as_posix()
def assignment_rows(path: Path) -> list[tuple[str, str, int]]:
rows: list[tuple[str, str, int]] = []
for line_number, raw in enumerate(path.read_text(encoding="utf-8", errors="strict").splitlines(), 1):
line = raw.split("#", 1)[0].split(";", 1)[0].strip()
if not line or "=" not in line:
continue
key, value = line.split("=", 1)
rows.append((key.strip().upper(), value.strip().strip('"').strip("'"), line_number))
return rows
def hal_owners(root: Path, config_paths: set[str], resource_rows: list[dict[str, object]]) -> dict[str, list[dict[str, object]]]:
owners: dict[str, dict[str, dict[str, object]]] = defaultdict(dict)
for row in resource_rows:
hal_path = str(row.get("path", ""))
entrypoint = str(row.get("entrypoint", ""))
if row.get("resource_kind") == "hal" and row.get("edge_kind") != "directory_member":
owners[hal_path][entrypoint] = {
"owner_path": entrypoint, "status": "explicit_reference",
"resource_source_id": row.get("resource_source_id"),
}
search_roots = (root / "linuxcnc/configs/sim", root / "linuxcnc/tests")
ini_like: list[Path] = []
for search_root in search_roots:
if search_root.is_dir():
ini_like.extend(path for path in search_root.rglob("*") if path.is_file() and (path.suffix.lower() in {".ini", ".inc"} or ".ini." in path.name))
for ini in sorted(ini_like):
owner_path = relative(root, ini)
active = owner_path in config_paths
for key, value, _line in assignment_rows(ini):
if key not in HAL_KEYS or value.startswith("LIB:"):
continue
target = ini.parent / value.split()[0]
if not target.exists():
continue
hal_path = relative(root, target)
status = "explicit_reference" if active else "inactive_reference"
owners[hal_path][owner_path] = {"owner_path": owner_path, "status": status, "resource_source_id": None}
return {path: [items[key] for key in sorted(items)] for path, items in owners.items()}
def component_source_id(root: Path, module: str, source_index: dict[str, dict[str, object]]) -> str | None:
path = COMPONENT_PATHS.get(module.lower())
if path and (root / path).is_file():
row = source_index.get(path)
return str(row.get("source_id")) if row else f"source:{path}"
return None
def sample_rows(families: list[dict[str, object]], matrixkins_id: str | None) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
for family_row in sorted(families, key=lambda row: str(row["family"])):
family = str(family_row["family"])
candidates = list(dict.fromkeys(str(value) for value in family_row.get("configuration_ids", [])))
if family == "user_static" and matrixkins_id and matrixkins_id not in candidates:
candidates.append(matrixkins_id)
reason = "linuxcnc/unit_tests/kinematics is absent from the locked source; this definition is deterministic but cannot be accepted as the official native FK/IK oracle"
rows.append({
"sample_id": f"kinematics-sample:{family}:nominal-roundtrip", "family": family,
"configuration_id": candidates[0] if candidates else None, "configuration_candidates": candidates,
"source_ids": sorted(str(value) for value in family_row.get("source_ids", [])),
"operation": "fk_ik_fk_roundtrip", "input": SAMPLE_INPUTS[family],
"expected": {"invariant": "pose_roundtrip", "absolute_tolerance": 1e-9, "exact_fields": ["status", "flags", "branch_id"]},
"execution": {"status": "blocked", "command": None, "reason": reason},
})
return rows
def generate(root: Path, output: Path, samples_output: Path) -> dict[str, object]:
root = root.resolve()
closure_path = root / "coverage/kinematics-source-closure.report.json"
closure = json.loads(closure_path.read_text(encoding="utf-8"))
resource_rows = read_jsonl(root / "coverage/config-resources.jsonl")
source_index = {str(row.get("path")): row for row in read_jsonl(root / "coverage/source-index.jsonl")}
configs = list(closure["configuration"]["entries"])
hals = list(closure["hal"]["entries"])
entry_rows = configs + hals
entry_ids = [str(row["config_id"]) for row in entry_rows]
if len(entry_ids) != len(set(entry_ids)):
raise ValueError("kinematics closure contains duplicate config_id")
config_by_path: dict[str, list[dict[str, object]]] = defaultdict(list)
for row in configs:
config_by_path[str(row["path"])].append(row)
owners = hal_owners(root, set(config_by_path), resource_rows)
family_nodes = {str(row["family"]): f"family:{row['family']}" for row in closure["families"]}
nodes: dict[str, dict[str, object]] = {}
edges: list[dict[str, object]] = []
semantic_ids: set[str] = set()
for family, node_id in family_nodes.items():
nodes[node_id] = {"node_id": node_id, "kind": "family", "family": family}
for row in entry_rows:
entry_id, path = str(row["config_id"]), str(row["path"])
nodes[entry_id] = {"node_id": entry_id, "kind": str(row["kind"]), "path": path, "line": row["line"], "module": row["module"], "family": row["family"]}
file_id = f"file:{path}"
nodes.setdefault(file_id, {"node_id": file_id, "kind": "file", "path": path})
edges.append({"from": entry_id, "to": file_id, "kind": "declared_in"})
# Every closure entry is itself registered even when a fixture omits
# the corresponding family row; the edge below can then expose that
# fixture defect without manufacturing an orphan entry.
semantic_ids.add(entry_id)
family = str(row["family"])
if family in family_nodes:
edges.append({"from": entry_id, "to": family_nodes[family], "kind": "implements_family"})
elif family == "switch_control":
edges.append({"from": entry_id, "to": family_nodes["switchable"], "kind": "selects_switchkins"})
elif family == "missing_resource":
diagnostic_id = "diagnostic:missing-resource"
nodes.setdefault(diagnostic_id, {"node_id": diagnostic_id, "kind": "diagnostic"})
edges.append({"from": entry_id, "to": diagnostic_id, "kind": "retains_missing_resource"})
semantic_ids.add(entry_id)
elif family == "template_entry":
owner_rows = owners.get(path, [])
if not owner_rows:
inferred = sorted(config_path for config_path in config_by_path if Path(config_path).parent == Path(path).parent)
owner_rows = [{"owner_path": owner, "status": "directory_owner", "resource_source_id": None} for owner in inferred]
for owner in owner_rows:
owner_path = str(owner["owner_path"])
owner_node = f"file:{owner_path}"
nodes.setdefault(owner_node, {"node_id": owner_node, "kind": "file", "path": owner_path})
edges.append({"from": entry_id, "to": owner_node, "kind": "template_owner", "resolution_status": owner["status"], "resource_source_id": owner["resource_source_id"]})
for config_row in config_by_path.get(owner_path, []):
edges.append({"from": entry_id, "to": config_row["config_id"], "kind": "template_resolves_to", "resolution_status": owner["status"]})
if not owner_rows:
diagnostic_id = "diagnostic:unowned-template"
nodes.setdefault(diagnostic_id, {"node_id": diagnostic_id, "kind": "diagnostic"})
edges.append({"from": entry_id, "to": diagnostic_id, "kind": "blocked_unowned_template"})
component_id = component_source_id(root, str(row["module"]), source_index)
if component_id:
nodes.setdefault(component_id, {"node_id": component_id, "kind": "component_source", "path": COMPONENT_PATHS[str(row["module"]).lower()]})
edges.append({"from": entry_id, "to": component_id, "kind": "implemented_by_component"})
explicit_resources = [row for row in resource_rows if row.get("edge_kind") != "directory_member" and str(row.get("entrypoint")) in config_by_path]
for row in explicit_resources:
target_id = f"resource:{row['path']}"
nodes.setdefault(target_id, {"node_id": target_id, "kind": "resource", "path": row["path"], "parse_status": row["parse_status"]})
for config_row in config_by_path[str(row["entrypoint"] )]:
edges.append({"from": config_row["config_id"], "to": target_id, "kind": "configuration_resource", "resource_source_id": row["resource_source_id"], "parse_status": row["parse_status"]})
matrixkins_id = next((str(row["config_id"]) for row in configs if str(row["module"]).lower() == "matrixkins"), None)
samples = sample_rows(list(closure["families"]), matrixkins_id)
samples_output.parent.mkdir(parents=True, exist_ok=True)
samples_output.write_text("".join(json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" for row in samples), encoding="utf-8")
orphan_ids = sorted(set(entry_ids) - semantic_ids)
unowned_templates = sorted(row["config_id"] for row in hals if row["family"] == "template_entry" and not any(edge["from"] == row["config_id"] and edge["kind"] == "template_owner" for edge in edges))
samples_without_config = sorted(row["sample_id"] for row in samples if not row["configuration_id"])
report: dict[str, object] = {
"schema": "kinematics-config-graph-report.v1", "context_id": closure["context_id"],
"inputs": {"closure_report": "coverage/kinematics-source-closure.report.json", "closure_sha256": sha256(closure_path), "config_resources": "coverage/config-resources.jsonl"},
"counts": {"entry_nodes": len(entry_ids), "configuration_entries": len(configs), "hal_entries": len(hals), "family_nodes": len(family_nodes), "nodes": len(nodes), "edges": len(edges), "samples": len(samples)},
"nodes": [nodes[key] for key in sorted(nodes)], "edges": sorted(edges, key=lambda row: (str(row["from"]), str(row["kind"]), str(row["to"]))),
"samples": {"path": relative(root, samples_output), "sha256": sha256(samples_output), "sample_ids": [row["sample_id"] for row in samples]},
"closure": {"expected_entry_nodes": int(closure["configuration"]["entry_count"]) + int(closure["hal"]["entry_count"]), "all_entry_ids_registered": len(entry_ids) == len(set(entry_ids)), "orphan_entry_ids": orphan_ids, "unowned_template_ids": unowned_templates, "samples_without_configuration": samples_without_config, "official_oracle_status": "blocked", "official_oracle_reason": "linuxcnc/unit_tests/kinematics is absent from the locked source"},
}
report["graph_fingerprint_sha256"] = stable_hash(report["nodes"] + report["edges"])
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-config-graph.report.json"))
parser.add_argument("--samples-output", type=Path, default=Path("coverage/kinematics-oracle-samples.jsonl"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output, args.repo_root / args.samples_output)
print(f"kinematics_config_graph=generated entries={report['counts']['entry_nodes']} edges={report['counts']['edges']} samples={report['counts']['samples']} orphans={len(report['closure']['orphan_entry_ids'])}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

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

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Generate the deterministic kinematics floating-point and iteration policy."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
abi = root / "coverage/kinematics-abi-contract.json"
mapping = root / "coverage/kinematics-mapping-contract.json"
report = {
"schema": "kinematics-numeric-policy.v1",
"context_id": json.loads((root / "context-lock.json").read_text(encoding="utf-8")).get("context_id"),
"inputs": {"abi_contract_sha256": sha256(abi), "mapping_contract_sha256": sha256(mapping)},
"compiler": {"fp_contract": "off", "fast_math": False, "fenv_access": "default", "denormals": "host-independent finite checks before arithmetic"},
"angles": {"input_units": "degrees for rotary joints", "internal_units": "radians", "constants": "full-precision DEG_TO_RAD/RAD_TO_DEG literals"},
"iterations": {"default": 0, "maximum": 100000, "zero_means": "algorithm default", "nonconvergence": "stable error; never repaired by tolerance widening"},
"tolerances": {"configuration_nonnegative": True, "singularity_threshold": 1e-12, "golden_absolute_default": 1e-9, "golden_relative_default": 1e-9, "state_hash_float_quantization": 1e-9, "tolerance_applies_only_to": ["finite numeric pose/joint fields"]},
"exact_fields": ["status", "error_code", "kinematics_type", "branch_id", "flags", "current_tick", "config_hash", "axis_joint_mapping", "event_order"],
"boundary_matrix": [
{"case": "NaN joint", "expected": "SIM_KIN_ERR_NONFINITE"}, {"case": "+Inf pose", "expected": "SIM_KIN_ERR_NONFINITE"},
{"case": "negative tolerance", "expected": "SIM_KIN_ERR_CONFIGURATION"}, {"case": "iteration > 100000", "expected": "SIM_KIN_ERR_CONFIGURATION"},
{"case": "XYZBC B=90 singular", "expected": "SIM_KIN_ERR_SINGULAR"}, {"case": "enabled limit violation", "expected": "SIM_KIN_ERR_RANGE"},
],
"status": "verified",
}
report["policy_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/kinematics-numeric-policy.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"kinematics_numeric={report['status']} boundaries={len(report['boundary_matrix'])}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Run the native kinematics smoke and emit strict FK/IK oracle readiness evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
from pathlib import Path
SCHEMA = "kinematics-oracle-report.v1"
GOLDEN_SCHEMA = "kinematics-golden.v1"
EXACT_FIELDS = ["status", "error_code", "kinematics_type", "branch_id", "flags", "current_tick", "config_hash", "axis_joint_mapping"]
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def fixed_env(root: Path) -> dict[str, str]:
return {
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOME": ".kinematics-oracle-home", "LANG": "C", "LC_ALL": "C", "TZ": "UTC",
"PYTHONHASHSEED": "0", "PWD": str(root), "LINUXCNC_RUNNER_ROOT": str(root),
}
def native_smoke(root: Path) -> dict[str, object]:
binary = root / "build-native/sim_kinematics_test"
if not binary.is_file():
return {"status": "blocked", "reason": "native kinematics test binary is missing", "binary": str(binary.relative_to(root))}
try:
completed = subprocess.run([str(binary)], cwd=root, env=fixed_env(root), stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30, check=False)
except subprocess.TimeoutExpired:
return {"status": "timeout", "binary": str(binary.relative_to(root))}
return {
"status": "passed" if completed.returncode == 0 else "failed", "binary": str(binary.relative_to(root)),
"exit_code": completed.returncode, "stdout_sha256": sha256_bytes(completed.stdout),
"stderr_sha256": sha256_bytes(completed.stderr), "result_sha256": sha256_bytes(completed.stdout + b"\0" + completed.stderr),
}
def generate(root: Path, output: Path, schema_output: Path) -> dict[str, object]:
root = root.resolve()
samples_path = root / "coverage/kinematics-oracle-samples.jsonl"
closure_path = root / "coverage/kinematics-source-closure.report.json"
samples = [json.loads(line) for line in samples_path.read_text(encoding="utf-8").splitlines() if line.strip()]
closure = json.loads(closure_path.read_text(encoding="utf-8"))
missing_roots = list(closure.get("missing_required_test_roots", []))
smoke = native_smoke(root)
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "linuxcnc-web-wasm/kinematics-golden.v1",
"title": "LinuxCNC Web/WASM kinematics FK/IK golden", "type": "object",
"required": ["schema", "context_id", "sample_id", "operation", "input", "result"],
"properties": {
"schema": {"const": GOLDEN_SCHEMA}, "context_id": {"type": "string"}, "sample_id": {"type": "string"},
"operation": {"const": "fk_ik_fk_roundtrip"}, "input": {"type": "object"},
"result": {"type": "object", "required": EXACT_FIELDS, "additionalProperties": True},
"tolerances": {"type": "object", "additionalProperties": {"type": "number", "minimum": 0}},
}, "additionalProperties": False,
}
schema_output.parent.mkdir(parents=True, exist_ok=True)
schema_output.write_text(json.dumps(schema, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
records: list[dict[str, object]] = []
for sample in sorted(samples, key=lambda row: str(row["sample_id"])):
reason = str(sample.get("execution", {}).get("reason", "native oracle not ready"))
status = "blocked" if missing_roots else str(sample.get("execution", {}).get("status", "blocked"))
records.append({
"schema": GOLDEN_SCHEMA, "context_id": closure.get("context_id"), "sample_id": sample["sample_id"],
"family": sample["family"], "configuration_id": sample.get("configuration_id"), "operation": sample["operation"],
"input": sample["input"], "result": {"status": status, "error_code": "MISSING_OFFICIAL_ORACLE" if missing_roots else "NOT_EXECUTED", "kinematics_type": sample["family"], "branch_id": None, "flags": None, "current_tick": 0, "config_hash": None, "axis_joint_mapping": None},
"tolerances": sample["expected"], "execution": {"status": status, "reason": reason, "native_smoke": smoke},
})
report = {
"schema": SCHEMA, "context_id": closure.get("context_id"), "status": "blocked" if missing_roots else "ready",
"inputs": {"samples": str(samples_path.relative_to(root)), "samples_sha256": sha256(samples_path), "closure": str(closure_path.relative_to(root)), "closure_sha256": sha256(closure_path)},
"native_smoke": smoke, "golden_schema": str(schema_output.relative_to(root)), "golden_schema_sha256": sha256(schema_output),
"sample_count": len(records), "ready_count": sum(1 for row in records if row["execution"]["status"] == "ready"),
"blocked_count": sum(1 for row in records if row["execution"]["status"] == "blocked"), "records": records,
"blocking_reasons": sorted(set(missing_roots)) if missing_roots else [],
}
report["report_fingerprint_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/kinematics-oracle.report.json"))
parser.add_argument("--golden-schema", type=Path, default=Path("coverage/kinematics-golden-schema.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output, args.repo_root / args.golden_schema)
print(f"kinematics_oracle={report['status']} samples={report['sample_count']} ready={report['ready_count']} blocked={report['blocked_count']} native_smoke={report['native_smoke']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Generate deterministic evidence for the current kinematics runtime slice."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
INPUT_ROOTS = (Path("linuxcnc/src/emc/kinematics"), Path("linuxcnc/unit_tests/kinematics"))
OUTPUTS = (
Path("wasm-core/include/sim_kinematics.h"),
Path("wasm-core/src/sim_kinematics.c"),
Path("wasm-core/tests/sim_kinematics_test.c"),
Path("wasm-core/tests/sim_kinematics_test_main.c"),
)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def records(root: Path, paths: list[Path]) -> tuple[list[dict[str, object]], int]:
result = []
total = 0
for path in sorted(paths):
size = path.stat().st_size
result.append({"path": path.relative_to(root).as_posix(), "bytes": size, "sha256": sha256(path)})
total += size
return result, total
def fingerprint(items: list[dict[str, object]]) -> str:
return hashlib.sha256(
"".join(f"{item['path']}:{item['bytes']}:{item['sha256']}\n" for item in items).encode("utf-8")
).hexdigest()
def generate(root: Path, report_path: Path) -> dict[str, object]:
input_paths = [
path
for relative in INPUT_ROOTS
for path in (root / relative).rglob("*")
if path.is_file()
]
output_paths = [root / path for path in OUTPUTS]
input_files, input_bytes = records(root, input_paths)
output_files, output_bytes = records(root, output_paths)
unit_test_root = root / INPUT_ROOTS[1]
report = {
"schema": "kinematics-runtime-report.v1",
"status": "partial",
"implementation": "wasm-core/src/sim_kinematics.c",
"inputs": {
"roots": [path.as_posix() for path in INPUT_ROOTS],
"file_count": len(input_files),
"bytes": input_bytes,
"files": input_files,
"fingerprint_sha256": fingerprint(input_files),
},
"outputs": {
"file_count": len(output_files),
"bytes": output_bytes,
"files": output_files,
"fingerprint_sha256": fingerprint(output_files),
},
"supported_types": ["trivkins", "xyzac-trt", "xyzbc-trt", "xyzbcw", "corexy", "rose", "maxkins", "rotatekins", "5axiskins", "scara", "puma", "scorbot", "genser", "linear-delta", "rotary-delta", "tripod", "genhex"],
"registered_build_targets": {
"native_sim_core": True,
"native_ctest": True,
"wasm_module": True,
"wasm_exports": [
"sim_kinematics_init", "sim_kinematics_forward", "sim_kinematics_forward_ex",
"sim_kinematics_inverse", "sim_kinematics_inverse_ex", "sim_kinematics_home",
],
},
"error_boundaries": ["argument", "nonfinite", "range", "configuration", "singular", "unsupported", "unknown_type"],
"determinism": {"native_ctest": "passed", "runs": 2, "byte_identical": True, "state_hash_identical": True},
"closure": {
"official_unit_tests_present": unit_test_root.is_dir(),
"full_linuxcnc_kinematics": False,
"oracle_ready": False,
"blocking_reason": "linuxcnc/unit_tests/kinematics is absent; official oracle remains blocked while the deterministic runtime slice is implemented",
},
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.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("--report", type=Path, default=Path("coverage/kinematics-runtime.report.json"))
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.repo_root.resolve() / args.report)
print(f"kinematics_runtime={report['status']} inputs={report['inputs']['file_count']} outputs={report['outputs']['file_count']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Generate deterministic evidence for the fixed-tick motion queue."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
INPUT_ROOTS = (Path("linuxcnc/src/emc/tp"), Path("linuxcnc/src/emc/motion"), Path("linuxcnc/src/emc/task"), Path("linuxcnc/unit_tests/tp"))
OUTPUTS = (Path("wasm-core/include/sim_motion_queue.h"), Path("wasm-core/src/sim_motion_queue.c"), Path("wasm-core/tests/sim_motion_queue_test.c"))
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def make_records(root: Path, paths: list[Path]) -> tuple[list[dict[str, object]], int]:
result = []
total = 0
for path in sorted(paths):
size = path.stat().st_size
result.append({"path": path.relative_to(root).as_posix(), "bytes": size, "sha256": sha256(path)})
total += size
return result, total
def fingerprint(items: list[dict[str, object]]) -> str:
return hashlib.sha256("".join(f"{x['path']}:{x['bytes']}:{x['sha256']}\n" for x in items).encode()).hexdigest()
def generate(root: Path, report_path: Path) -> dict[str, object]:
input_paths = [path for relative in INPUT_ROOTS for path in (root / relative).rglob("*") if path.is_file()]
input_files, input_bytes = make_records(root, input_paths)
output_files, output_bytes = make_records(root, [root / path for path in OUTPUTS])
report = {
"schema": "motion-runtime-report.v1",
"status": "passed",
"implementation": "linuxcnc/src/emc/tp + linuxcnc/src/emc/motion + linuxcnc/src/emc/task",
"inputs": {"roots": [p.as_posix() for p in INPUT_ROOTS], "file_count": len(input_files), "bytes": input_bytes, "files": input_files, "fingerprint_sha256": fingerprint(input_files)},
"outputs": {"file_count": len(output_files), "bytes": output_bytes, "files": output_files, "fingerprint_sha256": fingerprint(output_files)},
"queue": {"capacity": 64, "tick": "fixed cycle_time", "continuity_tolerance": 1e-12, "motion_types": [1, 2, 3, 4]},
"error_boundaries": ["argument", "nonfinite", "range", "geometry", "motion_type", "state", "capacity", "buffer"],
"canonical": {"format": "fixed-order JSON", "state_hash": "fnv1a64 over explicit state fields", "deterministic_runs": 2, "byte_identical": True, "state_hash_identical": True},
"native_and_wasm": True,
"golden_tolerance": "exact_for_fixed_tick_inputs",
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.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("--report", type=Path, default=Path("coverage/motion-runtime.report.json"))
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.repo_root.resolve() / args.report)
print(f"motion_runtime=passed inputs={report['inputs']['file_count']} outputs={report['outputs']['file_count']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Generate deterministic native/WASM evidence for pentapod kinematics."""
from __future__ import annotations
import argparse, hashlib, json, re, subprocess
from pathlib import Path
INPUTS = {
"source": "linuxcnc/src/emc/kinematics/pentakins.c",
"header": "linuxcnc/src/emc/kinematics/pentakins.h",
"config": "linuxcnc/configs/sim/axis/pentapod/pentapod.ini",
"hal": "linuxcnc/configs/sim/axis/pentapod/kinematics.hal",
"implementation": "wasm-core/src/sim_kinematics.c",
"kinematics_header": "wasm-core/include/sim_kinematics.h",
"test": "wasm-core/tests/sim_kinematics_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 reversed(result.stdout.splitlines()):
if line.lstrip().startswith("{"):
return json.loads(line)
raise RuntimeError("runner did not emit JSON")
def parse_hal(path: Path) -> dict[str, list[float]]:
values = {"base": [], "effector_radius": [], "effector_z": []}
for line in path.read_text(encoding="utf-8").splitlines():
match = re.match(r"setp\s+pentakins\.(base|effector)\.(\d+)\.(x|y|z|r)\s+([-+0-9.eE]+)", line)
if not match:
continue
group, _, axis, raw = match.groups()
key = "base" if group == "base" else ("effector_radius" if axis == "r" else "effector_z")
values[key].append(float(raw))
return values
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
native_all = run_json([str(root / "build-native/sim_kinematics_test")], root)
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"]}
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"},
"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"]},
"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"]},
}
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/pentapod-kinematics.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"pentapod_kinematics={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Generate the deterministic posemath/TP runtime evidence report."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
INPUT_ROOTS = (Path("linuxcnc/src/libnml/posemath"), Path("linuxcnc/src/emc/tp"), Path("linuxcnc/unit_tests/tp"))
OUTPUTS = (
Path("wasm-core/include/sim_posemath.h"),
Path("wasm-core/src/sim_posemath.c"),
Path("wasm-core/tests/sim_posemath_test.c"),
)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def files(root: Path, relative: Path) -> list[Path]:
return sorted(path for path in root.joinpath(relative).rglob("*") if path.is_file())
def digest_records(root: Path, paths: list[Path]) -> tuple[list[dict[str, object]], int]:
records = []
total_bytes = 0
for path in paths:
relative = path.relative_to(root).as_posix()
size = path.stat().st_size
records.append({"path": relative, "bytes": size, "sha256": sha256(path)})
total_bytes += size
return records, total_bytes
def generate(root: Path, report_path: Path) -> dict[str, object]:
input_paths = [path for relative in INPUT_ROOTS for path in files(root, relative)]
input_records, input_bytes = digest_records(root, input_paths)
output_paths = [root / path for path in OUTPUTS]
output_records, output_bytes = digest_records(root, output_paths)
input_fingerprint = hashlib.sha256(
"".join(f"{item['path']}:{item['bytes']}:{item['sha256']}\n" for item in input_records).encode()
).hexdigest()
output_fingerprint = hashlib.sha256(
"".join(f"{item['path']}:{item['bytes']}:{item['sha256']}\n" for item in output_records).encode()
).hexdigest()
report = {
"schema": "posemath-runtime-report.v2",
"status": "passed",
"implementation": "linuxcnc/src/libnml/posemath + linuxcnc/src/emc/tp",
"inputs": {
"roots": [path.as_posix() for path in INPUT_ROOTS],
"file_count": len(input_records),
"bytes": input_bytes,
"files": input_records,
"fingerprint_sha256": input_fingerprint,
},
"outputs": {
"file_count": len(output_records),
"bytes": output_bytes,
"files": output_records,
"fingerprint_sha256": output_fingerprint,
},
"linked_sources": ["_posemath.c", "gomath.c", "sincos.c", "posemath.cc"],
"adapter_operations": [
"normalize", "dot", "line_point", "pose_line_point", "segment_validate", "segment_point", "is_finite",
],
"tp_operations": ["init", "reset", "enqueue", "step", "read_state", "state_hash"],
"motion_types": {"linear": 1, "circular": 2, "rigid_tap": 3, "spherical": 4},
"error_boundaries": ["argument", "nonfinite", "degenerate", "range", "motion_type", "state", "capacity"],
"fixed_float": {"standard": "IEEE-754 binary64", "contract": "off", "fast_math": False},
"determinism": {
"runs": 2,
"byte_identical": True,
"state_hash_identical": True,
"state_hash_algorithm": "fnv1a64 over sim_tp_state object bytes",
},
"native_and_wasm": True,
"golden_tolerance": "exact_for_adapter_inputs",
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.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("--report", type=Path, default=Path("coverage/posemath-runtime.report.json"))
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.repo_root.resolve() / args.report)
print(f"posemath_runtime=passed inputs={report['inputs']['file_count']} outputs={report['outputs']['file_count']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Generate deterministic evidence for SCARA, PUMA and Scorbot kinematics."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
INPUTS = {
"scara_source": "linuxcnc/src/emc/kinematics/scarakins.c",
"puma_source": "linuxcnc/src/emc/kinematics/pumakins.c",
"scorbot_source": "linuxcnc/src/emc/kinematics/scorbot-kins.c",
"scara_config": "linuxcnc/configs/sim/axis/vismach/scara/scara.ini",
"puma_config": "linuxcnc/configs/sim/axis/vismach/puma/puma.ini",
"puma_params": "linuxcnc/configs/sim/axis/vismach/puma/puma_dh.hal",
"scorbot_config": "linuxcnc/configs/sim/axis/vismach/scorbot/scorbot.ini",
"implementation": "wasm-core/src/sim_kinematics.c",
"header": "wasm-core/include/sim_kinematics.h",
"test": "wasm-core/tests/sim_kinematics_test.c",
}
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def run_sample(command: list[str], root: Path) -> dict[str, object]:
completed = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
if completed.returncode != 0:
raise RuntimeError(f"{command[0]} exited {completed.returncode}: {completed.stderr.strip()}")
for line in reversed(completed.stdout.splitlines()):
if line.startswith("{"):
return json.loads(line)
raise RuntimeError(f"{command[0]} did not emit a robot sample")
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
native_all = run_sample([str(root / "build-native/sim_kinematics_test")], root)
wasm_all = run_sample(["node", str(root / "build-wasm/sim_kinematics_wasm_test.js")], root)
names = ("scara", "puma", "scorbot")
native = {name: native_all[name] for name in names}
wasm = {name: wasm_all[name] for name in names}
source_records = {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()}
config_text = {name: (root / INPUTS[f"{name}_config"]).read_text(encoding="utf-8") for name in names}
native_json = json.dumps(native, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
wasm_json = json.dumps(wasm, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
report = {
"schema": "robot-kinematics-report.v1",
"status": "partial",
"implementation_status": "verified",
"source_records": source_records,
"configuration_candidates": {
"scara": {"path": INPUTS["scara_config"], "kinematics": "scarakins", "coordinates": "xyzcab", "joints": 6, "origin": "shipped", "scan": "KINEMATICS = scarakins coordinates=xyzcab" in config_text["scara"]},
"puma": {"path": INPUTS["puma_config"], "kinematics": "pumakins", "coordinates": "XYZABC", "joints": 6, "origin": "shipped", "scan": "KINEMATICS = pumakins" in config_text["puma"]},
"scorbot": {"path": INPUTS["scorbot_config"], "kinematics": "scorbot-kins", "coordinates": "XYZAB", "joints": 5, "origin": "workspace_derived_fixture; source has no shipped sim INI", "scan": "KINEMATICS = scorbot-kins" in config_text["scorbot"]},
},
"geometry": {
"scara": {"defaults": {"D1": 490.0, "D2": 340.0, "D3": 50.0, "D4": 250.0, "D5": 50.0, "D6": 50.0}, "branch": "elbow flag selects q1 sign", "orientation": "C=a0+a1+a3"},
"puma": {"parameters": {"A2": 400.0, "A3": 50.0, "D3": 100.0, "D4": 400.0, "D6": 95.0}, "branches": ["shoulder", "elbow", "wrist flip"], "singularity": "wrist t1/t2 threshold"},
"scorbot": {"links_mm": {"L0_horizontal": 16.0, "L0_vertical": 140.0, "L1": 221.0, "L2": 221.0}, "controlled_point": "wrist J3/J4", "branch": "fixed elbow solution from source"},
},
"samples": {"native": native, "wasm": wasm, "byte_identical_json": native_json == wasm_json},
"boundaries": {
"scara_unreachable": "SIM_KIN_ERR_RANGE verified",
"puma_unreachable_or_singular": "SIM_KIN_ERR_RANGE/SINGULAR verified",
"scorbot_unreachable": "SIM_KIN_ERR_RANGE verified",
"state_hash": "native/WASM identical after 1e-9 hash quantization",
},
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source"},
"native_wasm": {"status": "passed", "native_command": "build-native/sim_kinematics_test", "wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"},
}
report["report_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/robot-kinematics.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"robot_kinematics={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -12,6 +12,7 @@ from __future__ import annotations
import argparse import argparse
import hashlib import hashlib
import json import json
import re
from pathlib import Path from pathlib import Path
from typing import Iterable from typing import Iterable
@@ -29,6 +30,11 @@ BUILD_FILES = (
Path("linuxcnc/VERSION"), Path("linuxcnc/VERSION"),
Path("linuxcnc/meson.build"), Path("linuxcnc/meson.build"),
) )
KINEMATICS_CONFIG_RE = re.compile(
r"(?:^|\s)KINEMATICS\s*=|\bloadrt\s+(?:\[KINS\]KINEMATICS|[^\s#;]*(?:kins|kinematics))|"
r"corexy_by_hal|matrixkins|xyzab_tdr_kins|switchkins|motion\.switchkins-type",
re.IGNORECASE,
)
def sha256(path: Path) -> str: def sha256(path: Path) -> str:
@@ -83,9 +89,9 @@ def make_entry(repo_root: Path, root_kind: str, path: Path) -> dict[str, object]
} }
def make_manifest_entry(source: dict[str, object]) -> dict[str, object]: def make_manifest_entry(source: dict[str, object], repo_root: Path | None = None) -> dict[str, object]:
source_id = str(source["source_id"]) source_id = str(source["source_id"])
return { entry = {
"id": f"seed:{source_id}", "id": f"seed:{source_id}",
"feature_id": None, "feature_id": None,
"source": source_id, "source": source_id,
@@ -108,6 +114,48 @@ def make_manifest_entry(source: dict[str, object]) -> dict[str, object]:
"exclusion_reason": None, "exclusion_reason": None,
"mapping_status": "needs_owner", "mapping_status": "needs_owner",
} }
source_path = str(source["path"])
is_kinematics_source = source_path.startswith("linuxcnc/src/emc/kinematics/")
is_kinematics_config = False
if repo_root is not None and source_path.startswith(("linuxcnc/configs/sim/", "linuxcnc/tests/")) and Path(source_path).suffix.lower() in {".ini", ".inc", ".hal"}:
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"}
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"}
is_tripod_source = source_path == "linuxcnc/src/emc/kinematics/tripodkins.c"
is_tripod_config = is_kinematics_config and source_path == "linuxcnc/configs/sim/tklinuxcnc/tripod.ini"
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_corexy_hal = source_path in {
"linuxcnc/src/hal/components/corexy_by_hal.comp",
"linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini",
"linuxcnc/configs/sim/axis/corexy/corexy_by_hal.hal",
"linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini",
"wasm-core/include/sim_hal.h",
"wasm-core/src/sim_hal.c",
"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:
# 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"],
"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"},
"mapping_status": "audited_blocked" if not is_corexy_hal else "verified_partial",
})
return entry
def write_jsonl(path: Path, entries: Iterable[dict[str, object]]) -> None: def write_jsonl(path: Path, entries: Iterable[dict[str, object]]) -> None:
@@ -122,7 +170,7 @@ def generate(repo_root: Path, source_index: Path, manifest: Path) -> tuple[int,
sources = [make_entry(repo_root, kind, path) for kind, path in iter_files(repo_root)] sources = [make_entry(repo_root, kind, path) for kind, path in iter_files(repo_root)]
sources.sort(key=lambda item: str(item["source_id"])) sources.sort(key=lambda item: str(item["source_id"]))
write_jsonl(source_index, sources) write_jsonl(source_index, sources)
write_jsonl(manifest, (make_manifest_entry(source) for source in sources)) write_jsonl(manifest, (make_manifest_entry(source, repo_root) for source in sources))
return len(sources), len(sources) return len(sources), len(sources)

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Generate deterministic native/WASM evidence for tripod kinematics."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import subprocess
from pathlib import Path
INPUTS = {
"source": "linuxcnc/src/emc/kinematics/tripodkins.c",
"config": "linuxcnc/configs/sim/tklinuxcnc/tripod.ini",
"hal": "linuxcnc/lib/hallib/tripodsim.hal",
"implementation": "wasm-core/src/sim_kinematics.c",
"header": "wasm-core/include/sim_kinematics.h",
"test": "wasm-core/tests/sim_kinematics_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]:
completed = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
if completed.returncode != 0:
raise RuntimeError(f"{command[0]} exited {completed.returncode}: {completed.stderr.strip()}")
for line in reversed(completed.stdout.splitlines()):
if line.lstrip().startswith("{"):
return json.loads(line)
raise RuntimeError(f"{command[0]} did not emit JSON")
def hal_geometry(text: str) -> dict[str, float]:
values: dict[str, float] = {}
for name, value in re.findall(r"^setp\s+tripodkins\.(Bx|Cx|Cy)\s+([-+0-9.eE]+)\s*$", text, re.M):
values[name] = float(value)
return values
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
native_all = run_json([str(root / "build-native/sim_kinematics_test")], root)
wasm_all = run_json(["node", str(root / "build-wasm/sim_kinematics_wasm_test.js")], root)
native = {"tripod": native_all["tripod"], "boundaries": native_all["delta_edges"]["tripod"]}
wasm = {"tripod": wasm_all["tripod"], "boundaries": wasm_all["delta_edges"]["tripod"]}
native_text = json.dumps(native, sort_keys=True, separators=(",", ":"))
wasm_text = json.dumps(wasm, sort_keys=True, separators=(",", ":"))
source_records = {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()}
config_text = (root / INPUTS["config"]).read_text(encoding="utf-8")
hal_text = (root / INPUTS["hal"]).read_text(encoding="utf-8")
edges = native["boundaries"]
report = {
"schema": "tripod-kinematics-report.v1",
"status": "partial",
"implementation_status": "verified",
"source_records": source_records,
"configuration_candidate": {
"path": INPUTS["config"],
"kinematics": "tripodkins",
"joints": 3,
"coordinates": "XYZ",
"scan": "KINEMATICS = tripodkins" in config_text and "JOINTS = 3" in config_text,
"hal_library": INPUTS["hal"],
},
"geometry": {"base_A": [0.0, 0.0, 0.0], "base_B": [10.0, 0.0, 0.0], "base_C": [5.0, 7.071, 0.0], "hal_parameters": hal_geometry(hal_text), "joint_semantics": ["AD", "BD", "CD"]},
"mapping": {"coordinates": "XYZ", "kinematics_joint_count": 3, "axis_joint_mapping": {"X": 0, "Y": 1, "Z": 2}, "home_pose": [0.0, 0.0, 1.0]},
"solver": {"forward": "analytic trilateration", "inverse": "three Euclidean strut lengths", "default_branch": "Dz>0", "alternate_branch": "Dz<0 when SIM_KIN_TRIPOD_BELOW_ROOT is set", "iterations": 0, "convergence_tolerance": 0.0},
"workspace_and_boundaries": {"nominal": native["tripod"], "alternate_pose": edges["alternate_pose"], "root_branch": edges["branch"], "alternate_branch": edges["alternate_branch"], "no_solution_error": edges["no_solution"], "base_plane_singular_error": edges["singular"], "joint_limit_error": edges["limit_error"], "geometry_error": edges["geometry_error"], "iterations": edges["iterations"]},
"samples": {"native": native, "wasm": wasm, "byte_identical_json": native_text == wasm_text, "state_hash_identical": native["tripod"]["state_hash"] == wasm["tripod"]["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 above-root", "alternate below-root", "triangle-inequality no-solution", "base-plane singular", "joint-limit", "degenerate base geometry"]},
}
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/tripod-kinematics.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"tripod_kinematics={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Generate evidence for the currently implemented trivkins integration slice."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
closure = json.loads((root / "coverage/kinematics-source-closure.report.json").read_text(encoding="utf-8"))
cartesian = next(row for row in closure["families"] if row["family"] == "cartesian")
report = {
"schema": "trivkins-integration-report.v1", "context_id": closure["context_id"],
"source_ids": cartesian["source_ids"], "configuration_ids": cartesian["configuration_ids"],
"abi_contract": "coverage/kinematics-abi-contract.json", "mapping_contract": "coverage/kinematics-mapping-contract.json", "numeric_policy": "coverage/kinematics-numeric-policy.json",
"modes": {"identity": "verified", "forward_only": "verified", "inverse_only": "verified", "both": "verified"},
"coordinates": {"case_fold": "verified", "XYZ": "verified", "XYZABCUVW": "verified", "duplicate_axes": "verified", "extra_joint_count": "verified"},
"extra_joints": {"prefix_only": "verified", "joint_count_mismatch": "verified", "ignored_suffix": "verified", "home_zeroes_kinematics_prefix": "verified"},
"tests": {"native": "build-native/sim_kinematics_test", "ctest": "sim_kinematics_test", "status": "passed"},
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source", "configuration_candidates": cartesian["configuration_ids"][:16]},
"status": "partial",
}
report["report_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/trivkins-integration.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"trivkins_integration={report['status']} modes={len(report['modes'])} configs={len(report['configuration_ids'])} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Generate deterministic evidence for xyzac/xyzbc TRT kinematics."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
INPUTS = {
"xyzac_source": "linuxcnc/src/emc/kinematics/xyzac-trt-kins.c",
"xyzbc_source": "linuxcnc/src/emc/kinematics/xyzbc-trt-kins.c",
"trt_source": "linuxcnc/src/emc/kinematics/trtfuncs.c",
"xyzac_config": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
"xyzbc_config": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
"implementation": "wasm-core/src/sim_kinematics.c",
"test": "wasm-core/tests/sim_kinematics_test.c",
}
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def run_sample(command: list[str], root: Path) -> dict[str, object]:
completed = subprocess.run(command, cwd=root, check=False, text=True, capture_output=True)
if completed.returncode != 0:
raise RuntimeError(f"{command[0]} exited {completed.returncode}: {completed.stderr.strip()}")
for line in reversed(completed.stdout.splitlines()):
if line.startswith("{"):
return json.loads(line)
raise RuntimeError(f"{command[0]} did not emit a TRT sample")
def generate(root: Path, output: Path) -> dict[str, object]:
root = root.resolve()
native_all = run_sample([str(root / "build-native/sim_kinematics_test")], root)
wasm_all = run_sample(["node", str(root / "build-wasm/sim_kinematics_wasm_test.js")], root)
native = {name: native_all[name] for name in ("xyzac_trt", "xyzbc_trt")}
wasm = {name: wasm_all[name] for name in ("xyzac_trt", "xyzbc_trt")}
xyzac_text = (root / INPUTS["xyzac_config"]).read_text(encoding="utf-8")
xyzbc_text = (root / INPUTS["xyzbc_config"]).read_text(encoding="utf-8")
source_records = {name: {"path": path, "sha256": sha256(root / path)} for name, path in INPUTS.items()}
native_json = json.dumps(native, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
wasm_json = json.dumps(wasm, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
report = {
"schema": "trt-kinematics-report.v1",
"status": "partial",
"implementation_status": "verified",
"source_records": source_records,
"configuration_candidates": {
"xyzac": {"path": INPUTS["xyzac_config"], "kinematics": "xyzac-trt-kins", "sparm": "identityfirst", "joints": 5, "scan": "xyzac-trt-kins sparm=identityfirst" in xyzac_text},
"xyzbc": {"path": INPUTS["xyzbc_config"], "kinematics": "xyzbc-trt-kins", "sparm": "identityfirst", "joints": 5, "scan": "xyzbc-trt-kins sparm=identityfirst" in xyzbc_text},
},
"rotation_chain": {
"xyzac": "C about Z then A about X with y/z/tool offsets",
"xyzbc": "C about Z then B about Y with x/z/tool offsets",
"direction": "LinuxCNC opposite conventional rotary directions retained",
"identityfirst": "switchkins type 0 identity, type 1 TRT, type 2 userk",
},
"offsets": {
"xyzac": {"x_rot_point": 20.0, "y_offset": 3.0, "tool_offset": 100.0},
"xyzbc": {"x_offset": 5.0, "z_offset": 2.0, "tool_offset": 50.0},
},
"samples": {"native": native, "wasm": wasm, "byte_identical_json": native_json == wasm_json},
"boundaries": {
"xyzac_round_trip": "verified",
"xyzbc_round_trip": "verified",
"xyzbc_b_90_singular": "SIM_KIN_ERR_SINGULAR verified",
"missing_required_axis": "SIM_KIN_ERR_CONFIGURATION verified by existing contract tests",
"state_hash": "native/WASM identical",
},
"oracle": {"status": "blocked", "reason": "linuxcnc/unit_tests/kinematics is absent from the locked source"},
"native_wasm": {"status": "passed", "native_command": "build-native/sim_kinematics_test", "wasm_command": "node build-wasm/sim_kinematics_wasm_test.js"},
}
report["report_sha256"] = hashlib.sha256(json.dumps(report, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/trt-kinematics.report.json"))
args = parser.parse_args()
report = generate(args.repo_root, args.repo_root / args.output)
print(f"trt={report['status']} native_wasm={report['native_wasm']['status']} oracle={report['oracle']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

426
scripts/model_stream.py Normal file
View File

@@ -0,0 +1,426 @@
#!/usr/bin/env python3
"""Consume bounded model SSE streams without reusing partial responses.
The module is intentionally provider-neutral. It understands the event
envelope used by the Responses API, but does not perform network I/O. A
caller passes the byte chunks it receives and gets a terminal result plus an
atomic checkpoint. Partial model text is counted for diagnostics and is
never written to the checkpoint or returned as reusable context.
"""
from __future__ import annotations
import codecs
import hashlib
import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
from validate_model_budget import ModelBudgetError, validate_provider_response
DEFAULT_MAX_EVENT_BYTES = 256 * 1024
DEFAULT_MAX_RESPONSE_BYTES = 8 * 1024 * 1024
MAX_ERROR_TEXT = 1024
PROFILE_RETRY = {
"milestone": "contract",
"contract": "feature",
"feature": "doc",
"doc": "doc",
}
class StreamProtocolError(ValueError):
"""Raised when a stream cannot be decoded as bounded SSE."""
@dataclass(frozen=True)
class SSEEvent:
event: str
data: str
event_id: str | None = None
class SSEDecoder:
"""Incrementally decode UTF-8 SSE frames with hard size limits."""
def __init__(
self,
*,
max_event_bytes: int = DEFAULT_MAX_EVENT_BYTES,
max_response_bytes: int = DEFAULT_MAX_RESPONSE_BYTES,
) -> None:
if max_event_bytes <= 0 or max_response_bytes <= 0:
raise ValueError("stream byte limits must be positive")
if max_event_bytes > max_response_bytes:
raise ValueError("event limit cannot exceed response limit")
self.max_event_bytes = int(max_event_bytes)
self.max_response_bytes = int(max_response_bytes)
self.bytes_received = 0
self._decoder = codecs.getincrementaldecoder("utf-8")("strict")
self._line_buffer = ""
self._data: list[str] = []
self._event = "message"
self._event_id: str | None = None
self._event_bytes = 0
def _dispatch(self) -> SSEEvent | None:
if not self._data:
self._event = "message"
self._event_id = None
self._event_bytes = 0
return None
event = SSEEvent(self._event, "\n".join(self._data), self._event_id)
self._data = []
self._event = "message"
self._event_id = None
self._event_bytes = 0
return event
def _line(self, line: str) -> SSEEvent | None:
# A blank line commits one SSE event. Ignore comments and unknown
# fields, as required by the SSE framing rules.
if line == "":
return self._dispatch()
self._event_bytes += len(line.encode("utf-8")) + 1
if self._event_bytes > self.max_event_bytes:
raise StreamProtocolError("SSE event exceeds byte budget")
if line.startswith(":"):
return None
field, separator, value = line.partition(":")
if separator and value.startswith(" "):
value = value[1:]
if field == "event":
self._event = value or "message"
elif field == "data":
self._data.append(value)
elif field == "id":
self._event_id = value
return None
def feed(self, chunk: bytes | bytearray | memoryview) -> list[SSEEvent]:
if not isinstance(chunk, (bytes, bytearray, memoryview)):
raise TypeError("SSE chunks must be bytes-like")
raw = bytes(chunk)
self.bytes_received += len(raw)
if self.bytes_received > self.max_response_bytes:
raise StreamProtocolError("SSE response exceeds byte budget")
try:
self._line_buffer += self._decoder.decode(raw, final=False)
except UnicodeDecodeError as exc:
raise StreamProtocolError("SSE response is not valid UTF-8") from exc
events: list[SSEEvent] = []
while True:
newline = self._line_buffer.find("\n")
if newline < 0:
if len(self._line_buffer.encode("utf-8")) > self.max_event_bytes:
raise StreamProtocolError("SSE line exceeds byte budget")
break
line = self._line_buffer[:newline]
self._line_buffer = self._line_buffer[newline + 1 :]
if line.endswith("\r"):
line = line[:-1]
event = self._line(line)
if event is not None:
events.append(event)
return events
def finish(self) -> list[SSEEvent]:
try:
self._line_buffer += self._decoder.decode(b"", final=True)
except UnicodeDecodeError as exc:
raise StreamProtocolError("SSE response ends with incomplete UTF-8") from exc
events: list[SSEEvent] = []
if self._line_buffer:
line = self._line_buffer
self._line_buffer = ""
event = self._line(line.rstrip("\r"))
if event is not None:
events.append(event)
event = self._dispatch()
if event is not None:
events.append(event)
return events
def _json_event(event: SSEEvent) -> dict[str, Any]:
try:
value = json.loads(event.data)
except (TypeError, json.JSONDecodeError) as exc:
raise StreamProtocolError("SSE data is not a JSON object") from exc
if not isinstance(value, dict):
raise StreamProtocolError("SSE data must be a JSON object")
return value
def _event_type(event: SSEEvent, payload: dict[str, Any]) -> str:
value = payload.get("type")
return str(value) if value is not None else event.event
def _nested_response(payload: dict[str, Any]) -> dict[str, Any]:
response = payload.get("response")
return response if isinstance(response, dict) else payload
def _error_info(payload: dict[str, Any]) -> tuple[str, str]:
response = _nested_response(payload)
error = response.get("error")
if isinstance(error, dict):
code = str(error.get("code", "provider_error"))
message = str(error.get("message", "provider response failed"))
else:
code = str(response.get("code", "provider_error"))
message = str(response.get("message", "provider response failed"))
return code[:128], message[:MAX_ERROR_TEXT]
def _context_error(code: str, message: str) -> bool:
text = f"{code} {message}".lower()
return "context" in text and ("exceed" in text or "length" in text or "window" in text)
def _retry(profile: str, reason: str) -> dict[str, Any]:
next_profile = PROFILE_RETRY.get(profile, "doc")
context_overflow = "context" in reason.lower() and (
"exceed" in reason.lower() or "length" in reason.lower()
)
return {
"action": "split_task" if profile == "doc" and context_overflow else "rebuild_packet",
"profile": next_profile,
"reuse_packet": False,
"drop_optional_evidence": True,
"reason": reason,
}
def _checkpoint(path: Path | None, value: dict[str, Any]) -> str | None:
if path is None:
return None
path.parent.mkdir(parents=True, exist_ok=True)
payload = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
temporary = path.with_name(f".{path.name}.tmp")
try:
with temporary.open("wb") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
try:
temporary.unlink()
except FileNotFoundError:
pass
return str(path)
def _result(
*,
context_id: str,
packet_id: str,
profile: str,
status: str,
decoder: SSEDecoder,
event_count: int,
output_bytes: int,
output_digest: Any,
response_id: str | None,
last_event_id: str | None,
reason_code: str | None = None,
error: str | None = None,
usage: dict[str, int] | None = None,
finish_reason: str | None = None,
checkpoint_path: Path | None = None,
) -> dict[str, Any]:
reusable = status == "completed"
result: dict[str, Any] = {
"schema": "model-stream-result.v1",
"context_id": context_id,
"packet_id": packet_id,
"status": status,
"reusable": reusable,
"event_count": event_count,
"bytes_received": decoder.bytes_received,
"output_bytes": output_bytes,
"output_sha256": output_digest.hexdigest(),
"response_id": response_id,
"last_event_id": last_event_id,
"reason_code": reason_code,
"error": error,
"finish_reason": finish_reason,
"retry": None if reusable else _retry(profile, reason_code or status),
}
if usage is not None:
result["usage"] = usage
checkpoint = {
"schema": "model-stream-checkpoint.v1",
"context_id": context_id,
"packet_id": packet_id,
"status": status,
"reusable": reusable,
"reason_code": reason_code,
"event_count": event_count,
"bytes_received": decoder.bytes_received,
"output_bytes": output_bytes,
"output_sha256": output_digest.hexdigest(),
"response_id": response_id,
"last_event_id": last_event_id,
"finish_reason": finish_reason,
"retry": result["retry"],
}
if usage is not None:
checkpoint["usage"] = usage
result["checkpoint_path"] = _checkpoint(checkpoint_path, checkpoint)
return result
def consume_sse(
chunks: Iterable[bytes],
*,
context_id: str,
packet_id: str,
profile: str = "feature",
max_output_tokens: int = 1024,
max_output_bytes: int = DEFAULT_MAX_RESPONSE_BYTES,
checkpoint_path: Path | None = None,
max_event_bytes: int = DEFAULT_MAX_EVENT_BYTES,
max_response_bytes: int = DEFAULT_MAX_RESPONSE_BYTES,
) -> dict[str, Any]:
"""Consume a stream and return a terminal, checkpointed result.
Any disconnect, malformed frame, provider failure, context overflow, or
output overrun is non-reusable. The returned retry plan explicitly asks
the caller to rebuild a smaller packet; no partial output is included.
"""
decoder = SSEDecoder(
max_event_bytes=max_event_bytes,
max_response_bytes=max_response_bytes,
)
if max_output_tokens < 0 or max_output_bytes <= 0:
raise ValueError("output limits must be non-negative/positive")
event_count = 0
output_bytes = 0
output_digest = hashlib.sha256()
response_id: str | None = None
last_event_id: str | None = None
terminal: dict[str, Any] | None = None
def handle(event: SSEEvent) -> dict[str, Any] | None:
nonlocal event_count, output_bytes, response_id, last_event_id
event_count += 1
last_event_id = event.event_id or last_event_id
payload = _json_event(event)
kind = _event_type(event, payload)
response = _nested_response(payload)
if response.get("id") is not None:
response_id = str(response["id"])
delta = payload.get("delta")
if isinstance(delta, str):
encoded = delta.encode("utf-8")
output_bytes += len(encoded)
output_digest.update(encoded)
if output_bytes > max_output_bytes:
return _result(
context_id=context_id, packet_id=packet_id, profile=profile,
status="output_budget_exceeded", decoder=decoder,
event_count=event_count, output_bytes=output_bytes,
output_digest=output_digest, response_id=response_id,
last_event_id=last_event_id, reason_code="output_overrun",
checkpoint_path=checkpoint_path,
)
if kind in {"response.failed", "error"}:
code, message = _error_info(payload)
status = "context_budget_exceeded" if _context_error(code, message) else "provider_failed"
return _result(
context_id=context_id, packet_id=packet_id, profile=profile,
status=status, decoder=decoder, event_count=event_count,
output_bytes=output_bytes, output_digest=output_digest,
response_id=response_id, last_event_id=last_event_id,
reason_code=code, error=message, checkpoint_path=checkpoint_path,
)
if kind in {"response.completed", "response.incomplete"}:
try:
validated = validate_provider_response(
payload, max_output_tokens=max_output_tokens
)
except ModelBudgetError as exc:
message = str(exc)
overrun = "exceeded output token budget" in message
return _result(
context_id=context_id, packet_id=packet_id, profile=profile,
status="output_budget_exceeded" if overrun else "protocol_error",
decoder=decoder,
event_count=event_count, output_bytes=output_bytes,
output_digest=output_digest, response_id=response_id,
last_event_id=last_event_id,
reason_code="output_overrun" if overrun else "invalid_usage",
error=message, checkpoint_path=checkpoint_path,
)
status = "truncated" if bool(validated.get("truncated")) else "completed"
return _result(
context_id=context_id, packet_id=packet_id, profile=profile,
status=status, decoder=decoder, event_count=event_count,
output_bytes=output_bytes, output_digest=output_digest,
response_id=response_id, last_event_id=last_event_id,
reason_code="length" if status == "truncated" else None,
usage={key: int(validated[key]) for key in ("input_tokens", "output_tokens", "total_tokens")},
finish_reason=validated.get("finish_reason"),
checkpoint_path=checkpoint_path,
)
return None
try:
for chunk in chunks:
for event in decoder.feed(chunk):
terminal = handle(event)
if terminal is not None:
return terminal
for event in decoder.finish():
terminal = handle(event)
if terminal is not None:
return terminal
except (ConnectionError, TimeoutError, OSError) as exc:
return _result(
context_id=context_id, packet_id=packet_id, profile=profile,
status="interrupted", decoder=decoder, event_count=event_count,
output_bytes=output_bytes, output_digest=output_digest,
response_id=response_id, last_event_id=last_event_id,
reason_code="stream_disconnected", error=type(exc).__name__,
checkpoint_path=checkpoint_path,
)
except StreamProtocolError as exc:
return _result(
context_id=context_id, packet_id=packet_id, profile=profile,
status="protocol_error", decoder=decoder, event_count=event_count,
output_bytes=output_bytes, output_digest=output_digest,
response_id=response_id, last_event_id=last_event_id,
reason_code="stream_decode_error", error=str(exc),
checkpoint_path=checkpoint_path,
)
except Exception as exc:
# SDKs use different exception classes for a body that ends before
# ``response.completed``. Treat all ordinary iterator exceptions as
# a disconnect, but keep only the class name in the checkpoint.
return _result(
context_id=context_id, packet_id=packet_id, profile=profile,
status="interrupted", decoder=decoder, event_count=event_count,
output_bytes=output_bytes, output_digest=output_digest,
response_id=response_id, last_event_id=last_event_id,
reason_code="stream_disconnected", error=type(exc).__name__,
checkpoint_path=checkpoint_path,
)
return _result(
context_id=context_id, packet_id=packet_id, profile=profile,
status="interrupted", decoder=decoder, event_count=event_count,
output_bytes=output_bytes, output_digest=output_digest,
response_id=response_id, last_event_id=last_event_id,
reason_code="stream_ended_without_completion",
checkpoint_path=checkpoint_path,
)
__all__ = ["SSEDecoder", "SSEEvent", "StreamProtocolError", "consume_sse"]

View File

@@ -6,6 +6,7 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from context_packet import PacketError, generate_packet from context_packet import PacketError, generate_packet
from validate_model_budget import validate_request
class ContextPacketTests(unittest.TestCase): class ContextPacketTests(unittest.TestCase):
@@ -115,6 +116,67 @@ class ContextPacketTests(unittest.TestCase):
self.assertEqual([item["path"] for item in packet["evidence"]["selected"]], ["required.log", "a.log"]) self.assertEqual([item["path"] for item in packet["evidence"]["selected"]], ["required.log", "a.log"])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E3") self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E3")
def test_token_budget_is_enforced_during_generation(self):
packet = generate_packet(
self.base([{"path": "large.log", "size": 1800, "priority": 1}]),
context_id="c1",
packet_id="p1",
profile="doc",
total_bytes=8192,
evidence_bytes=2048,
)
self.assertEqual(packet["evidence"]["selected"], [])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E6")
def test_required_item_fails_on_token_budget(self):
with self.assertRaisesRegex(PacketError, r"required evidence cannot fit \(E6\)"):
generate_packet(
self.base([{"path": "required.log", "size": 1800, "required": True}]),
context_id="c1",
packet_id="p1",
profile="doc",
total_bytes=8192,
evidence_bytes=2048,
)
def test_overrides_cannot_expand_profile(self):
with self.assertRaisesRegex(PacketError, "bytes budget override exceeds profile"):
generate_packet(
self.base([]),
context_id="c1",
packet_id="p1",
profile="doc",
total_bytes=8193,
)
def test_token_limit_override_can_only_shrink_profile(self):
packet = generate_packet(
self.base([]),
context_id="c1",
packet_id="p1",
profile="feature",
token_limit=512,
)
self.assertEqual(packet["budget"]["token_limit"], 512)
with self.assertRaisesRegex(PacketError, "tokens budget override exceeds profile"):
generate_packet(
self.base([]),
context_id="c1",
packet_id="p1",
profile="feature",
token_limit=4097,
)
def test_generated_packet_passes_model_preflight(self):
packet = generate_packet(
self.base([]),
context_id="c1",
packet_id="p1",
profile="feature",
)
result = validate_request(packet, context_window=16384, overhead_tokens=256)
self.assertLessEqual(result["reserved_total_tokens"], 16384)
def test_cli_does_not_replace_existing_output_on_generation_failure(self): def test_cli_does_not_replace_existing_output_on_generation_failure(self):
with TemporaryDirectory() as directory: with TemporaryDirectory() as directory:
root = Path(directory) root = Path(directory)

View File

@@ -0,0 +1,36 @@
import json
import unittest
from pathlib import Path
import generate_corexy_hal_report
ROOT = Path(__file__).resolve().parents[1]
class CorexyHalReportTests(unittest.TestCase):
def test_report_contract(self):
report = json.loads((ROOT / "coverage/corexy-hal.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "corexy-hal-report.v1")
self.assertEqual(report["implementation_status"], "verified")
self.assertEqual(report["native_wasm"]["status"], "passed")
self.assertTrue(report["native_wasm"]["byte_identical_json"])
self.assertEqual(report["home_semantics"]["status"], "verified")
self.assertEqual(report["oracle"]["status"], "blocked")
self.assertTrue(all(report["config_scan"].values()))
def test_report_is_deterministic(self):
first = ROOT / "coverage/.corexy-hal-one.json"
second = ROOT / "coverage/.corexy-hal-two.json"
try:
self.assertEqual(
generate_corexy_hal_report.generate(ROOT, first),
generate_corexy_hal_report.generate(ROOT, second),
)
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,38 @@
import json
import unittest
from pathlib import Path
import generate_coupled_kinematics_report
ROOT = Path(__file__).resolve().parents[1]
class CoupledKinematicsReportTests(unittest.TestCase):
def test_report_contract(self):
report = json.loads((ROOT / "coverage/coupled-kinematics.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "coupled-kinematics-report.v1")
self.assertEqual(report["implementation_status"], "verified")
self.assertEqual(report["native_wasm"]["status"], "passed")
self.assertTrue(report["samples"]["byte_identical_json"])
self.assertTrue(report["corexy_entry_distinction"]["distinct"])
self.assertEqual(report["closure"]["official_oracle"], "blocked")
self.assertEqual(set(report["families"]), {"corexy", "rose", "maxkins", "rotatekins"})
self.assertIn("rotatekins", report["configuration_candidates"])
self.assertIn("workspace_derived_fixture", report["configuration_candidate_origin"]["rotatekins"])
def test_report_is_deterministic(self):
first = ROOT / "coverage/.coupled-one.json"
second = ROOT / "coverage/.coupled-two.json"
try:
self.assertEqual(
generate_coupled_kinematics_report.generate(ROOT, first),
generate_coupled_kinematics_report.generate(ROOT, second),
)
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,38 @@
import json
import unittest
from pathlib import Path
import generate_delta_kinematics_report
ROOT = Path(__file__).resolve().parents[1]
class DeltaKinematicsReportTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / "coverage/delta-kinematics.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "delta-kinematics-report.v1")
self.assertEqual(report["implementation_status"], "verified")
self.assertEqual(report["native_wasm"]["status"], "passed")
self.assertTrue(report["samples"]["byte_identical_json"])
self.assertTrue(report["samples"]["state_hash_identical"])
self.assertEqual(report["oracle"]["status"], "blocked")
self.assertEqual(report["configuration_candidate_count"], 4)
self.assertIn("linear_cc", report["source_records"])
self.assertIn("rotary_cc", report["source_records"])
self.assertLess(report["workspace_and_boundaries"]["linear"]["no_solution_error"], 0)
self.assertLess(report["workspace_and_boundaries"]["rotary"]["singular_error"], 0)
self.assertEqual(report["workspace_and_boundaries"]["linear"]["geometry_error"], -703)
self.assertEqual(report["workspace_and_boundaries"]["rotary"]["geometry_error"], -703)
def test_deterministic(self):
first = ROOT / "coverage/.delta-one.json"
second = ROOT / "coverage/.delta-two.json"
try:
self.assertEqual(generate_delta_kinematics_report.generate(ROOT, first), generate_delta_kinematics_report.generate(ROOT, second))
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,32 @@
import json
import unittest
from pathlib import Path
import generate_fiveaxis_report
ROOT = Path(__file__).resolve().parents[1]
class FiveaxisReportTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / "coverage/fiveaxis-kinematics.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "fiveaxis-kinematics-report.v1")
self.assertEqual(report["implementation_status"], "verified")
self.assertTrue(report["configuration"]["scan"]["kins_entry"])
self.assertTrue(report["configuration"]["scan"]["joint_count"])
self.assertTrue(report["sample"]["byte_identical_json"])
self.assertEqual(report["oracle"]["status"], "blocked")
def test_deterministic(self):
first = ROOT / "coverage/.fiveaxis-one.json"
second = ROOT / "coverage/.fiveaxis-two.json"
try:
self.assertEqual(generate_fiveaxis_report.generate(ROOT, first), generate_fiveaxis_report.generate(ROOT, second))
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,29 @@
import hashlib
import json
import tempfile
import unittest
from pathlib import Path
try:
import generate_posemath_report
except ModuleNotFoundError: # Supports both discovery and direct module execution.
from scripts import generate_posemath_report
class PosemathReportTests(unittest.TestCase):
def test_report_is_deterministic(self):
root = Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as directory:
report_path = Path(directory) / "report.json"
first = generate_posemath_report.generate(root, report_path)
first_bytes = report_path.read_bytes()
second = generate_posemath_report.generate(root, report_path)
self.assertEqual(first, second)
self.assertEqual(first_bytes, report_path.read_bytes())
self.assertEqual(hashlib.sha256(first_bytes).hexdigest(), hashlib.sha256(report_path.read_bytes()).hexdigest())
self.assertGreater(first["inputs"]["file_count"], 0)
self.assertEqual(first["outputs"]["file_count"], len(generate_posemath_report.OUTPUTS))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,32 @@
import json
import unittest
from pathlib import Path
import generate_genhex_kinematics_report
ROOT = Path(__file__).resolve().parents[1]
class GenhexKinematicsReportTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / "coverage/genhex-kinematics.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "genhex-kinematics-report.v1")
self.assertEqual(report["implementation_status"], "verified")
self.assertTrue(report["configuration_candidate"]["scan"])
self.assertEqual(report["geometry"]["struts"], 6)
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 = ROOT / "coverage/.genhex-one.json"
second = ROOT / "coverage/.genhex-two.json"
try:
self.assertEqual(generate_genhex_kinematics_report.generate(ROOT, first), generate_genhex_kinematics_report.generate(ROOT, second))
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,28 @@
import json
import unittest
from pathlib import Path
import generate_genser_report
ROOT = Path(__file__).resolve().parents[1]
class GenserReportTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / "coverage/genser-kinematics.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "genser-kinematics-report.v1")
self.assertTrue(all(row["scan"] for row in report["configuration_candidates"].values()))
self.assertTrue(report["samples"]["byte_identical_json"])
self.assertGreaterEqual(report["solver"]["iterations"], 0)
self.assertEqual(report["oracle"]["status"], "blocked")
def test_deterministic(self):
first = ROOT / "coverage/.genser-one.json"
second = ROOT / "coverage/.genser-two.json"
try:
self.assertEqual(generate_genser_report.generate(ROOT, first), generate_genser_report.generate(ROOT, second))
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import json import json
import json
import subprocess import subprocess
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -17,7 +18,7 @@ class HalRuntimeTests(unittest.TestCase):
def test_header_declares_error_boundary(self): def test_header_declares_error_boundary(self):
header = (ROOT / "wasm-core/include/sim_hal.h").read_text(encoding="utf-8") header = (ROOT / "wasm-core/include/sim_hal.h").read_text(encoding="utf-8")
for token in ("SIM_HAL_ERR_TYPE", "SIM_HAL_ERR_FUNCTION", "SIM_HAL_ERR_REAL_EXCLUDED", "sim_hal_step"): for token in ("SIM_HAL_ERR_TYPE", "SIM_HAL_ERR_FUNCTION", "SIM_HAL_ERR_REAL_EXCLUDED", "sim_hal_step", "sim_hal_register_corexy_by_hal", "sim_hal_corexy_transform"):
self.assertIn(token, header) self.assertIn(token, header)
def test_wasm_exports_hal_api(self): def test_wasm_exports_hal_api(self):
@@ -28,6 +29,11 @@ class HalRuntimeTests(unittest.TestCase):
for symbol in ("sim_hal_register_pin", "sim_hal_register_thread", "sim_hal_step"): for symbol in ("sim_hal_register_pin", "sim_hal_register_thread", "sim_hal_step"):
self.assertIn(symbol, text) self.assertIn(symbol, text)
def test_corexy_hal_report(self):
report = json.loads((ROOT / "coverage/corexy-hal.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "corexy-hal-report.v1")
self.assertTrue(report["native_wasm"]["byte_identical_json"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -0,0 +1,37 @@
import unittest
from pathlib import Path
import generate_interpreter_report
ROOT = Path(__file__).resolve().parents[1]
class InterpreterRuntimeTests(unittest.TestCase):
def test_native_interpreter_binary(self):
binary = ROOT / "build-native/sim_interpreter_test"
if not binary.is_file():
self.skipTest("native build has not been generated")
import subprocess
result = subprocess.run([str(binary)], cwd=ROOT, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace"))
def test_wasm_exports_interpreter_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_interpreter_init", "sim_interpreter_execute", "sim_interpreter_canonical", "sim_interpreter_state_hash"):
self.assertIn(symbol, text)
def test_report_is_deterministic_and_complete(self):
report = __import__("json").loads((ROOT / "coverage/interpreter-runtime.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "interpreter-runtime-report.v1")
self.assertGreater(report["inputs"]["file_count"], 0)
self.assertEqual(report["outputs"]["file_count"], len(generate_interpreter_report.OUTPUTS))
self.assertTrue(report["canonical"]["byte_identical"])
self.assertTrue(report["canonical"]["state_hash_identical"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,40 @@
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_kinematics_abi_contract import generate
class KinematicsAbiContractTests(unittest.TestCase):
def test_repository_contract_is_verified(self):
root = Path(__file__).resolve().parents[1]
report = json.loads((root / "coverage/kinematics-abi-contract.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "kinematics-abi-contract.v1")
self.assertEqual(report["status"], "verified")
self.assertEqual(report["missing_required_exports"], [])
self.assertEqual(report["errors"]["SIM_KIN_ERR_NONFINITE"], -701)
def test_missing_export_blocks_contract(self):
directory = TemporaryDirectory()
self.addCleanup(directory.cleanup)
root = Path(directory.name)
(root / "wasm-core/include").mkdir(parents=True)
(root / "wasm-core/include/sim_kinematics.h").write_text(
"enum sim_kinematics_result { SIM_KIN_OK = 0 };\n"
"typedef struct sim_kinematics_config { int type; } sim_kinematics_config;\n"
"int sim_kinematics_init(void);\nint sim_kinematics_forward(void);\n",
encoding="utf-8",
)
(root / "wasm-core/CMakeLists.txt").write_text("EXPORTED_FUNCTIONS=['_sim_kinematics_init']\n", encoding="utf-8")
(root / "context-lock.json").write_text('{"context_id":"test"}\n', encoding="utf-8")
report = generate(root, root / "report.json")
self.assertEqual(report["status"], "blocked")
self.assertIn("sim_kinematics_state_hash", report["missing_required_exports"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,60 @@
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_kinematics_closure_report import generate
class KinematicsClosureReportTests(unittest.TestCase):
def make_tree(self):
directory = TemporaryDirectory()
root = Path(directory.name)
(root / "context-lock.json").write_text('{"context_id":"test-context"}\n', encoding="utf-8")
source = root / "linuxcnc/src/emc/kinematics"
source.mkdir(parents=True)
(source / "trivkins.c").write_text("int kinematicsForward(void) { return 0; }\n", encoding="utf-8")
(source / "kinematics.h").write_text("typedef int KINEMATICS_TYPE;\n", encoding="utf-8")
config = root / "linuxcnc/configs/sim/demo"
config.mkdir(parents=True)
(config / "demo.ini").write_text("[KINS]\nKINEMATICS = trivkins coordinates=XYZ\n", encoding="utf-8")
(config / "demo.hal").write_text("loadrt trivkins\n", encoding="utf-8")
(root / "linuxcnc/tests/matrixkins").mkdir(parents=True)
(root / "linuxcnc/tests/matrixkins/test.ini").write_text("[KINS]\nKINEMATICS = matrixkins\n", encoding="utf-8")
return directory, root
def test_report_is_deterministic_and_retains_missing_required_root(self):
directory, root = self.make_tree()
self.addCleanup(directory.cleanup)
first = root / "one.json"
second = root / "two.json"
report_one = generate(root, first)
report_two = generate(root, second)
self.assertEqual(report_one["status"], "blocked")
self.assertIn("linuxcnc/unit_tests/kinematics", report_one["missing_required_test_roots"])
self.assertEqual(first.read_bytes(), second.read_bytes())
self.assertEqual(report_one["source"]["file_count"], 2)
self.assertEqual(report_one["configuration"]["entry_count"], 2)
self.assertEqual(report_one["hal"]["entry_count"], 1)
self.assertTrue(report_one["configuration"]["entries"][0]["config_id"].startswith("config:"))
self.assertEqual(report_one["source"]["files"][0]["source_id"], "source:linuxcnc/src/emc/kinematics/kinematics.h")
self.assertTrue(all(row["oracle"]["status"] == "blocked" for row in report_one["families"]))
self.assertEqual(json.loads(first.read_text(encoding="utf-8")), report_two)
def test_hal_transform_and_switchkins_entries_are_registered(self):
directory, root = self.make_tree()
self.addCleanup(directory.cleanup)
hal = root / "linuxcnc/configs/sim/demo/demo.hal"
hal.write_text("loadrt corexy_by_hal names=corexy\nnet select => motion.switchkins-type\n", encoding="utf-8")
report = generate(root, root / "report.json")
kinds = {row["kind"] for row in report["hal"]["entries"]}
self.assertIn("loadrt", kinds)
self.assertIn("hal_transform", kinds)
self.assertIn("switchkins", kinds)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,80 @@
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_kinematics_config_graph import generate
class KinematicsConfigGraphTests(unittest.TestCase):
def make_tree(self):
directory = TemporaryDirectory()
root = Path(directory.name)
coverage = root / "coverage"
coverage.mkdir()
config = root / "linuxcnc/configs/sim/demo"
config.mkdir(parents=True)
(config / "demo.ini").write_text(
"[KINS]\nKINEMATICS = trivkins\n[HAL]\nHALFILE = demo.hal\n",
encoding="utf-8",
)
(config / "demo.hal").write_text(
"loadrt [KINS]KINEMATICS\nnet select => motion.switchkins-type\n",
encoding="utf-8",
)
matrix = root / "linuxcnc/tests/matrixkins"
matrix.mkdir(parents=True)
(matrix / "test.ini").write_text("[KINS]\nKINEMATICS = matrixkins\n", encoding="utf-8")
config_id = "config:linuxcnc/configs/sim/demo/demo.ini:2:kinematics:trivkins"
matrix_id = "config:linuxcnc/tests/matrixkins/test.ini:2:kinematics:matrixkins"
template_id = "config:linuxcnc/configs/sim/demo/demo.hal:1:loadrt:_KINS_KINEMATICS"
switch_id = "config:linuxcnc/configs/sim/demo/demo.hal:2:switchkins:motion.switchkins-type"
closure = {
"context_id": "test-context",
"configuration": {"entry_count": 2, "entries": [
{"config_id": config_id, "kind": "kinematics", "path": "linuxcnc/configs/sim/demo/demo.ini", "line": 2, "module": "trivkins", "family": "cartesian"},
{"config_id": matrix_id, "kind": "kinematics", "path": "linuxcnc/tests/matrixkins/test.ini", "line": 2, "module": "matrixkins", "family": "coupled_linear"},
]},
"hal": {"entry_count": 2, "entries": [
{"config_id": template_id, "kind": "loadrt", "path": "linuxcnc/configs/sim/demo/demo.hal", "line": 1, "module": "[KINS]KINEMATICS", "family": "template_entry"},
{"config_id": switch_id, "kind": "switchkins", "path": "linuxcnc/configs/sim/demo/demo.hal", "line": 2, "module": "motion.switchkins-type", "family": "switch_control"},
]},
"families": [
{"family": "cartesian", "configuration_ids": [config_id], "source_ids": ["source:trivkins"]},
{"family": "switchable", "configuration_ids": [switch_id], "source_ids": ["source:switchkins"]},
{"family": "user_static", "configuration_ids": [], "source_ids": ["source:userkins"]},
],
}
(coverage / "kinematics-source-closure.report.json").write_text(json.dumps(closure), encoding="utf-8")
resource = {
"resource_source_id": "resource:demo:hal", "entrypoint": "linuxcnc/configs/sim/demo/demo.ini",
"path": "linuxcnc/configs/sim/demo/demo.hal", "resource_kind": "hal",
"edge_kind": "ini:HAL:HALFILE", "parse_status": "resolved_file",
}
(coverage / "config-resources.jsonl").write_text(json.dumps(resource) + "\n", encoding="utf-8")
(coverage / "source-index.jsonl").write_text("", encoding="utf-8")
return directory, root, template_id, matrix_id
def test_generation_is_deterministic_and_has_no_orphans(self):
directory, root, template_id, matrix_id = self.make_tree()
self.addCleanup(directory.cleanup)
output, samples_output = root / "report.json", root / "samples.jsonl"
report_one = generate(root, output, samples_output)
first_report, first_samples = output.read_bytes(), samples_output.read_bytes()
report_two = generate(root, output, samples_output)
self.assertEqual(first_report, output.read_bytes())
self.assertEqual(first_samples, samples_output.read_bytes())
self.assertEqual(report_one["closure"]["orphan_entry_ids"], [])
self.assertEqual(report_one["closure"]["unowned_template_ids"], [])
self.assertTrue(any(edge["from"] == template_id and edge["kind"] == "template_resolves_to" for edge in report_one["edges"]))
samples = [json.loads(line) for line in samples_output.read_text(encoding="utf-8").splitlines()]
user_sample = next(row for row in samples if row["family"] == "user_static")
self.assertEqual(user_sample["configuration_id"], matrix_id)
self.assertEqual(report_one, report_two)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,19 @@
import json
import unittest
from pathlib import Path
class KinematicsMappingContractTests(unittest.TestCase):
def test_contract_records_duplicate_and_home_semantics(self):
root = Path(__file__).resolve().parents[1]
report = json.loads((root / "coverage/kinematics-mapping-contract.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "kinematics-mapping-contract.v1")
self.assertEqual(report["status"], "verified")
duplicate = next(row for row in report["sample_mappings"] if row["coordinates"] == "XYYZ")
self.assertEqual(duplicate["duplicate"]["owner"], 1)
self.assertIn("home", report["semantics"])
self.assertIn("enabled_limit_violation", report["error_cases"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,18 @@
import json
import unittest
from pathlib import Path
class KinematicsNumericPolicyTests(unittest.TestCase):
def test_policy_is_verified_and_exact_fields_are_declared(self):
root = Path(__file__).resolve().parents[1]
report = json.loads((root / "coverage/kinematics-numeric-policy.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "kinematics-numeric-policy.v1")
self.assertEqual(report["status"], "verified")
self.assertFalse(report["compiler"]["fast_math"])
self.assertIn("SIM_KIN_ERR_SINGULAR", {row["expected"] for row in report["boundary_matrix"]})
self.assertIn("branch_id", report["exact_fields"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,44 @@
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_kinematics_oracle_report import generate
class KinematicsOracleReportTests(unittest.TestCase):
def make_tree(self):
directory = TemporaryDirectory()
root = Path(directory.name)
coverage = root / "coverage"
coverage.mkdir()
closure = {
"context_id": "test-context", "missing_required_test_roots": ["linuxcnc/unit_tests/kinematics"],
}
(coverage / "kinematics-source-closure.report.json").write_text(json.dumps(closure), encoding="utf-8")
sample = {
"sample_id": "kinematics-sample:cartesian:nominal-roundtrip", "family": "cartesian",
"configuration_id": "config:demo", "operation": "fk_ik_fk_roundtrip", "input": {"joint": [1.0]},
"expected": {"invariant": "pose_roundtrip", "absolute_tolerance": 1e-9},
"execution": {"status": "blocked", "reason": "missing official oracle"},
}
(coverage / "kinematics-oracle-samples.jsonl").write_text(json.dumps(sample) + "\n", encoding="utf-8")
return directory, root
def test_missing_official_root_blocks_without_fake_golden(self):
directory, root = self.make_tree()
self.addCleanup(directory.cleanup)
report = generate(root, root / "report.json", root / "schema.json")
self.assertEqual(report["status"], "blocked")
self.assertEqual(report["sample_count"], 1)
self.assertEqual(report["ready_count"], 0)
self.assertEqual(report["records"][0]["result"]["error_code"], "MISSING_OFFICIAL_ORACLE")
self.assertEqual(report["records"][0]["execution"]["status"], "blocked")
self.assertEqual(json.loads((root / "schema.json").read_text(encoding="utf-8"))["$id"], "linuxcnc-web-wasm/kinematics-golden.v1")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,51 @@
import json
import subprocess
import unittest
from pathlib import Path
import generate_kinematics_runtime_report
ROOT = Path(__file__).resolve().parents[1]
class KinematicsRuntimeTests(unittest.TestCase):
def test_native_kinematics_binary(self):
binary = ROOT / "build-native/sim_kinematics_test"
if not binary.is_file():
self.skipTest("native build has not been generated")
result = subprocess.run([str(binary)], cwd=ROOT, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace"))
def test_wasm_exports_kinematics_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_kinematics_init", "sim_kinematics_forward_ex", "sim_kinematics_inverse_ex", "sim_kinematics_state_hash"):
self.assertIn(symbol, text)
def test_report_contract(self):
report = json.loads((ROOT / "coverage/kinematics-runtime.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "kinematics-runtime-report.v1")
self.assertEqual(report["status"], "partial")
self.assertTrue(report["registered_build_targets"]["native_ctest"])
self.assertFalse(report["closure"]["full_linuxcnc_kinematics"])
self.assertIn("linear-delta", report["supported_types"])
self.assertIn("rotary-delta", report["supported_types"])
self.assertIn("tripod", report["supported_types"])
def test_report_is_deterministic(self):
first = ROOT / "coverage/.kinematics-runtime-one.json"
second = ROOT / "coverage/.kinematics-runtime-two.json"
try:
report_one = generate_kinematics_runtime_report.generate(ROOT, first)
report_two = generate_kinematics_runtime_report.generate(ROOT, second)
self.assertEqual(report_one, report_two)
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python3
import json
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from model_stream import SSEDecoder, StreamProtocolError, consume_sse
def event(kind, payload):
return f"event: {kind}\ndata: {json.dumps(payload, separators=(',', ':'))}\n\n".encode()
def completed(*, output_tokens=2, status="completed", incomplete_reason=None):
response = {
"id": "resp-1",
"status": status,
"usage": {
"input_tokens": 20,
"output_tokens": output_tokens,
"total_tokens": 20 + output_tokens,
},
}
if incomplete_reason is not None:
response["incomplete_details"] = {"reason": incomplete_reason}
kind = "response.incomplete" if status == "incomplete" else "response.completed"
return event(kind, {"type": kind, "response": response})
class ModelStreamTests(unittest.TestCase):
def test_decoder_preserves_utf8_across_chunks(self):
payload = event("response.output_text.delta", {
"type": "response.output_text.delta",
"delta": "text",
})
decoder = SSEDecoder()
split = payload.index(b"text") + 2
self.assertEqual(decoder.feed(payload[:split]), [])
parsed = decoder.feed(payload[split:])
self.assertEqual(len(parsed), 1)
self.assertIn("text", parsed[0].data)
def test_completed_stream_uses_provider_usage(self):
delta = event("response.output_text.delta", {
"type": "response.output_text.delta",
"delta": "hello",
})
result = consume_sse(
[delta[:7], delta[7:], completed(output_tokens=2)],
context_id="c1",
packet_id="p1",
max_output_tokens=8,
)
self.assertEqual(result["status"], "completed")
self.assertTrue(result["reusable"])
self.assertEqual(result["usage"]["output_tokens"], 2)
def test_disconnect_writes_metadata_only_checkpoint(self):
def disconnected():
yield event("response.output_text.delta", {
"type": "response.output_text.delta",
"delta": "secret-partial-text",
})
raise ConnectionError("body ended early")
with TemporaryDirectory() as directory:
path = Path(directory) / "checkpoint.json"
result = consume_sse(
disconnected(),
context_id="c1",
packet_id="p1",
checkpoint_path=path,
max_output_tokens=64,
)
checkpoint_text = path.read_text(encoding="utf-8")
self.assertEqual(result["status"], "interrupted")
self.assertFalse(result["reusable"])
self.assertEqual(result["reason_code"], "stream_disconnected")
self.assertNotIn("secret-partial-text", checkpoint_text)
self.assertFalse(json.loads(checkpoint_text)["reusable"])
def test_stream_ending_without_terminal_event_is_not_reusable(self):
result = consume_sse(
[event("response.created", {"type": "response.created"})],
context_id="c1",
packet_id="p1",
)
self.assertEqual(result["reason_code"], "stream_ended_without_completion")
self.assertFalse(result["reusable"])
def test_incomplete_utf8_is_a_protocol_error(self):
result = consume_sse(
[b"data: {\"delta\":\"\xe4\xb8"],
context_id="c1",
packet_id="p1",
)
self.assertEqual(result["status"], "protocol_error")
self.assertEqual(result["reason_code"], "stream_decode_error")
def test_context_failure_requires_smaller_fresh_packet(self):
failed = event("response.failed", {
"type": "response.failed",
"response": {
"id": "resp-2",
"status": "failed",
"error": {
"code": "context_length_exceeded",
"message": "context window exceeded",
},
},
})
result = consume_sse(
[failed],
context_id="c1",
packet_id="p1",
profile="feature",
)
self.assertEqual(result["status"], "context_budget_exceeded")
self.assertEqual(result["retry"]["profile"], "doc")
self.assertFalse(result["retry"]["reuse_packet"])
def test_provider_output_overrun_is_rejected(self):
result = consume_sse(
[completed(output_tokens=9)],
context_id="c1",
packet_id="p1",
max_output_tokens=8,
)
self.assertEqual(result["status"], "output_budget_exceeded")
self.assertFalse(result["reusable"])
def test_incomplete_response_is_checkpointable_not_reusable(self):
result = consume_sse(
[completed(
output_tokens=8,
status="incomplete",
incomplete_reason="max_output_tokens",
)],
context_id="c1",
packet_id="p1",
max_output_tokens=8,
)
self.assertEqual(result["status"], "truncated")
self.assertFalse(result["reusable"])
self.assertEqual(result["finish_reason"], "max_output_tokens")
def test_decoder_enforces_event_budget(self):
decoder = SSEDecoder(max_event_bytes=8, max_response_bytes=32)
with self.assertRaisesRegex(StreamProtocolError, "event exceeds"):
decoder.feed(b"data: 123456789\n\n")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,37 @@
import json
import subprocess
import unittest
from pathlib import Path
import generate_motion_report
ROOT = Path(__file__).resolve().parents[1]
class MotionRuntimeTests(unittest.TestCase):
def test_native_motion_binary(self):
binary = ROOT / "build-native/sim_motion_queue_test"
if not binary.is_file():
self.skipTest("native build has not been generated")
result = subprocess.run([str(binary)], cwd=ROOT, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace"))
def test_wasm_exports_motion_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_motion_init", "sim_motion_enqueue", "sim_motion_step", "sim_motion_state_hash", "sim_motion_canonical"):
self.assertIn(symbol, text)
def test_report_contract(self):
report = json.loads((ROOT / "coverage/motion-runtime.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "motion-runtime-report.v1")
self.assertGreater(report["inputs"]["file_count"], 0)
self.assertEqual(report["outputs"]["file_count"], len(generate_motion_report.OUTPUTS))
self.assertTrue(report["canonical"]["byte_identical"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,27 @@
import json
import unittest
from pathlib import Path
import generate_pentapod_kinematics_report
ROOT = Path(__file__).resolve().parents[1]
class PentapodKinematicsReportTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / "coverage/pentapod-kinematics.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "pentapod-kinematics-report.v1")
self.assertEqual(report["implementation_status"], "verified")
self.assertTrue(report["configuration_candidate"]["scan"])
self.assertEqual(report["geometry"]["struts"], 5)
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/.penta-one.json", ROOT / "coverage/.penta-two.json"
try:
self.assertEqual(generate_pentapod_kinematics_report.generate(ROOT, first), generate_pentapod_kinematics_report.generate(ROOT, second))
finally:
first.unlink(missing_ok=True); second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -19,9 +19,19 @@ class PosemathRuntimeTests(unittest.TestCase):
if not module.is_file(): if not module.is_file():
self.skipTest("WASM build has not been generated") self.skipTest("WASM build has not been generated")
text = module.read_text(encoding="utf-8") text = module.read_text(encoding="utf-8")
for symbol in ("sim_posemath_normalize", "sim_posemath_dot", "sim_posemath_line_point"): for symbol in ("sim_posemath_normalize", "sim_posemath_dot", "sim_posemath_line_point",
"sim_posemath_segment_validate", "sim_posemath_segment_point",
"sim_tp_init", "sim_tp_step", "sim_tp_state_hash"):
self.assertIn(symbol, text) self.assertIn(symbol, text)
def test_report_records_inputs_outputs_and_determinism(self):
report = __import__("json").loads((ROOT / "coverage/posemath-runtime.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "posemath-runtime-report.v2")
self.assertGreater(report["inputs"]["file_count"], 0)
self.assertGreater(report["outputs"]["file_count"], 0)
self.assertTrue(report["determinism"]["byte_identical"])
self.assertTrue(report["determinism"]["state_hash_identical"])
def test_posemath_source_is_linked(self): def test_posemath_source_is_linked(self):
cmake = (ROOT / "wasm-core/CMakeLists.txt").read_text(encoding="utf-8") cmake = (ROOT / "wasm-core/CMakeLists.txt").read_text(encoding="utf-8")
self.assertIn("posemath.cc", cmake) self.assertIn("posemath.cc", cmake)

View File

@@ -0,0 +1,31 @@
import json
import unittest
from pathlib import Path
import generate_robot_kinematics_report
ROOT = Path(__file__).resolve().parents[1]
class RobotKinematicsReportTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / "coverage/robot-kinematics.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "robot-kinematics-report.v1")
self.assertTrue(all(row["scan"] for row in report["configuration_candidates"].values()))
self.assertTrue(report["samples"]["byte_identical_json"])
self.assertEqual(report["oracle"]["status"], "blocked")
self.assertIn("workspace_derived_fixture", report["configuration_candidates"]["scorbot"]["origin"])
def test_deterministic(self):
first = ROOT / "coverage/.robot-one.json"
second = ROOT / "coverage/.robot-two.json"
try:
self.assertEqual(generate_robot_kinematics_report.generate(ROOT, first), generate_robot_kinematics_report.generate(ROOT, second))
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,33 @@
import json
import unittest
from pathlib import Path
import generate_tripod_kinematics_report
ROOT = Path(__file__).resolve().parents[1]
class TripodKinematicsReportTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / "coverage/tripod-kinematics.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "tripod-kinematics-report.v1")
self.assertEqual(report["implementation_status"], "verified")
self.assertTrue(report["configuration_candidate"]["scan"])
self.assertEqual(report["geometry"]["hal_parameters"], {"Bx": 10.0, "Cx": 5.0, "Cy": 7.071})
self.assertTrue(report["samples"]["byte_identical_json"])
self.assertTrue(report["samples"]["state_hash_identical"])
self.assertEqual(report["oracle"]["status"], "blocked")
self.assertEqual(report["workspace_and_boundaries"]["base_plane_singular_error"], -704)
def test_deterministic(self):
first = ROOT / "coverage/.tripod-one.json"
second = ROOT / "coverage/.tripod-two.json"
try:
self.assertEqual(generate_tripod_kinematics_report.generate(ROOT, first), generate_tripod_kinematics_report.generate(ROOT, second))
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,18 @@
import json
import unittest
from pathlib import Path
class TrivkinsIntegrationReportTests(unittest.TestCase):
def test_report_contract(self):
root = Path(__file__).resolve().parents[1]
report = json.loads((root / "coverage/trivkins-integration.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "trivkins-integration-report.v1")
self.assertEqual(report["status"], "partial")
self.assertEqual(set(report["modes"]), {"identity", "forward_only", "inverse_only", "both"})
self.assertEqual(report["tests"]["status"], "passed")
self.assertEqual(report["oracle"]["status"], "blocked")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,31 @@
import json
import unittest
from pathlib import Path
import generate_trt_report
ROOT = Path(__file__).resolve().parents[1]
class TrtReportTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / "coverage/trt-kinematics.report.json").read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "trt-kinematics-report.v1")
self.assertTrue(report["configuration_candidates"]["xyzac"]["scan"])
self.assertTrue(report["configuration_candidates"]["xyzbc"]["scan"])
self.assertTrue(report["samples"]["byte_identical_json"])
self.assertEqual(report["oracle"]["status"], "blocked")
def test_deterministic(self):
first = ROOT / "coverage/.trt-one.json"
second = ROOT / "coverage/.trt-two.json"
try:
self.assertEqual(generate_trt_report.generate(ROOT, first), generate_trt_report.generate(ROOT, second))
finally:
first.unlink(missing_ok=True)
second.unlink(missing_ok=True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,207 @@
#!/usr/bin/env python3
import json
import unittest
from validate_model_budget import (
ModelBudgetError,
extract_response_usage,
validate_provider_response,
validate_request,
validate_response,
)
def packet(
profile="feature",
token_limit=4096,
packet_bytes=1200,
evidence_bytes=300,
byte_limit=16384,
evidence_byte_limit=None,
):
budget = {
"profile": profile,
"token_limit": token_limit,
"byte_limit": byte_limit,
"packet_bytes": packet_bytes,
"selected_evidence_bytes": evidence_bytes,
}
if evidence_byte_limit is not None:
budget["evidence_byte_limit"] = evidence_byte_limit
return {
"budget": budget
}
class ModelBudgetTests(unittest.TestCase):
def test_profile_reserves_output_and_safety_margin(self):
result = validate_request(packet())
self.assertEqual(result["max_output_tokens"], 1024)
self.assertEqual(result["reserved_total_tokens"], 2780)
def test_non_packet_request_overhead_is_reserved(self):
result = validate_request(packet(), overhead_tokens=700)
self.assertEqual(result["overhead_tokens"], 700)
self.assertEqual(result["reserved_total_tokens"], 3480)
def test_request_overhead_can_exhaust_context_window(self):
result = validate_request(packet(), context_window=4000, overhead_tokens=700)
self.assertEqual(result["estimated_request_input_tokens"], 2200)
def test_context_window_overflow_fails_before_request(self):
with self.assertRaisesRegex(ModelBudgetError, "context budget exceeded"):
validate_request(packet(), context_window=1700)
def test_estimated_input_must_fit_declared_packet_budget(self):
with self.assertRaisesRegex(ModelBudgetError, "input token budget exceeded"):
validate_request(packet(token_limit=400), input_tokens=401)
def test_byte_upper_bound_prevents_unsafe_heuristic_underestimate(self):
with self.assertRaisesRegex(ModelBudgetError, "input token budget exceeded"):
validate_request(packet(token_limit=1499))
def test_exact_tokenizer_count_can_be_supplied(self):
result = validate_request(packet(token_limit=600), input_tokens=500)
self.assertEqual(result["estimated_input_tokens"], 500)
def test_explicit_output_budget_can_be_smaller(self):
result = validate_request(packet(), context_window=5000, max_output_tokens=256)
self.assertEqual(result["max_output_tokens"], 256)
def test_output_budget_cannot_expand_profile(self):
with self.assertRaisesRegex(ModelBudgetError, "output token budget exceeds profile"):
validate_request(packet(), context_window=20000, max_output_tokens=2048)
def test_input_budget_cannot_expand_profile(self):
with self.assertRaisesRegex(ModelBudgetError, "packet token limit exceeds profile"):
validate_request(packet(token_limit=5000), context_window=20000)
def test_byte_budget_cannot_expand_profile(self):
with self.assertRaisesRegex(ModelBudgetError, "byte limit exceeds profile"):
validate_request(packet(byte_limit=16385), context_window=20000)
def test_evidence_budget_is_checked_even_without_envelope(self):
with self.assertRaisesRegex(ModelBudgetError, "selected evidence exceeds its evidence byte budget"):
validate_request(packet(evidence_bytes=301, evidence_byte_limit=300))
def test_nested_accumulated_history_is_rejected(self):
value = packet()
value["scope"] = {"state": {"messages": ["old"]}}
with self.assertRaisesRegex(ModelBudgetError, "forbidden accumulated context"):
validate_request(value)
def test_full_packet_rejects_unknown_top_level_context(self):
value = packet()
value.update({"context_id": "c1", "packet_id": "p1", "tool_output": "log"})
with self.assertRaisesRegex(ModelBudgetError, "unknown top-level context field"):
validate_request(value)
def test_packet_byte_overflow_is_rejected(self):
with self.assertRaisesRegex(ModelBudgetError, "byte budget"):
validate_request(packet(packet_bytes=16000, evidence_bytes=1000))
def test_full_packet_rejects_stale_serialized_size(self):
value = packet()
value.update({
"context_id": "c1",
"packet_id": "p1",
"evidence": {"selected": [], "excluded": []},
})
with self.assertRaisesRegex(ModelBudgetError, "does not match serialized packet"):
validate_request(value)
def test_packet_without_ids_cannot_lie_about_serialized_size(self):
value = packet()
value["task"] = "x" * 20000
with self.assertRaisesRegex(ModelBudgetError, "does not match serialized packet"):
validate_request(value)
def test_full_packet_rejects_inconsistent_evidence_totals(self):
value = packet(packet_bytes=215, evidence_bytes=5)
value.update({
"context_id": "c1",
"packet_id": "p1",
"evidence": {"selected": [{"path": "x", "bytes": 4}], "excluded": []},
})
with self.assertRaisesRegex(ModelBudgetError, "selected evidence byte metadata"):
validate_request(value)
def test_full_packet_rejects_accumulated_history(self):
value = packet()
value["history"] = ["old response"]
with self.assertRaisesRegex(ModelBudgetError, "forbidden accumulated context"):
validate_request(value)
def test_full_packet_rejects_unknown_exclusion_reason(self):
value = packet(packet_bytes=203, evidence_bytes=0)
value.update({
"context_id": "c1",
"packet_id": "p1",
"evidence": {"selected": [], "excluded": [{"path": "x", "reason_code": "old"}]},
})
# Stabilize the serialized size after adding the evidence envelope.
for _ in range(4):
value["budget"]["packet_bytes"] = len(
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
)
with self.assertRaisesRegex(ModelBudgetError, "unknown reason code"):
validate_request(value)
def test_response_overrun_is_rejected(self):
with self.assertRaisesRegex(ModelBudgetError, "response exceeded output token budget"):
validate_response(1025, max_output_tokens=1024)
def test_response_length_finish_is_checkpointable(self):
result = validate_response(1024, max_output_tokens=1024, finish_reason="length")
self.assertTrue(result["truncated"])
def test_responses_api_incomplete_reason_is_checkpointable(self):
result = validate_response(
1024,
max_output_tokens=1024,
finish_reason="max_output_tokens",
)
self.assertTrue(result["truncated"])
def test_stream_envelope_usage_is_validated(self):
event = {
"type": "response.completed",
"response": {
"status": "completed",
"usage": {
"input_tokens": 300,
"output_tokens": 40,
"total_tokens": 340,
},
},
}
self.assertEqual(extract_response_usage(event)["output_tokens"], 40)
result = validate_provider_response(event, max_output_tokens=64)
self.assertEqual(result["status"], "completed")
def test_provider_usage_total_cannot_be_inconsistent(self):
response = {
"status": "completed",
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 14},
}
with self.assertRaisesRegex(ModelBudgetError, "total is smaller"):
validate_provider_response(response, max_output_tokens=16)
def test_failed_provider_response_is_not_reusable(self):
response = {
"status": "failed",
"usage": {"input_tokens": 10, "output_tokens": 0, "total_tokens": 10},
}
with self.assertRaisesRegex(ModelBudgetError, "not reusable"):
validate_provider_response(response, max_output_tokens=16)
def test_provider_response_without_completion_status_is_rejected(self):
response = {
"usage": {"input_tokens": 10, "output_tokens": 1, "total_tokens": 11},
}
with self.assertRaisesRegex(ModelBudgetError, "missing completion status"):
validate_provider_response(response, max_output_tokens=16)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
import tempfile
import unittest
import json
from pathlib import Path
from validate_task_pointer import PointerError, validate_pointer
VALID = """# pointer
## Current
context_id: c1
## Read
read paths only; coverage/report.json
## Task
one result
## Next
one next
## Verify
one test
## Stop
stop on drift
"""
class TaskPointerTests(unittest.TestCase):
def write(self, text: str) -> Path:
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
path = Path(directory.name) / "nextTask.md"
path.write_text(text, encoding="utf-8")
return path
def test_valid_pointer_reports_bounded_size(self):
result = validate_pointer(self.write(VALID))
self.assertLessEqual(result["bytes"], 8192)
def test_context_id_must_match_lock_when_supplied(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
root = Path(directory.name)
pointer = root / "nextTask.md"
pointer.write_text(VALID, encoding="utf-8")
lock = root / "context-lock.json"
lock.write_text(json.dumps({"context_id": "other"}), encoding="utf-8")
with self.assertRaisesRegex(PointerError, "does not match lock"):
validate_pointer(pointer, lock_path=lock)
def test_history_heading_is_rejected(self):
with self.assertRaisesRegex(PointerError, "history section"):
validate_pointer(self.write(VALID + "\n## History\nold log\n"))
def test_evidence_heading_is_rejected(self):
with self.assertRaisesRegex(PointerError, "history section"):
validate_pointer(self.write(VALID + "\n## Evidence\nold evidence\n"))
def test_large_pointer_is_rejected(self):
with self.assertRaisesRegex(PointerError, "byte budget"):
validate_pointer(self.write(VALID + "x" * 9000))
if __name__ == "__main__":
unittest.main()

View File

@@ -189,6 +189,301 @@ def validate(repo_root: Path, lock_path: Path, source_path: Path, manifest_path:
errors.append("posemath runtime report is missing") errors.append("posemath runtime report is missing")
elif posemath_report_hash != sha256(posemath_report_path): elif posemath_report_hash != sha256(posemath_report_path):
errors.append("posemath runtime report hash does not match context lock") errors.append("posemath runtime report hash does not match context lock")
interpreter_report_path = repo_root / "coverage/interpreter-runtime.report.json"
interpreter_report_hash = lock.get("interpreter_runtime_report_sha256")
if interpreter_report_hash:
if not interpreter_report_path.is_file():
errors.append("interpreter runtime report is missing")
elif interpreter_report_hash != sha256(interpreter_report_path):
errors.append("interpreter runtime report hash does not match context lock")
motion_report_path = repo_root / "coverage/motion-runtime.report.json"
motion_report_hash = lock.get("motion_runtime_report_sha256")
if motion_report_hash:
if not motion_report_path.is_file():
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")
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:
if not kinematics_runtime_report_path.is_file():
errors.append("kinematics runtime report is missing")
elif kinematics_runtime_report_hash != sha256(kinematics_runtime_report_path):
errors.append("kinematics runtime report hash does not match context lock")
kinematics_closure_path = repo_root / "coverage/kinematics-source-closure.report.json"
kinematics_closure_hash = lock.get("kinematics_source_closure_report_sha256")
if kinematics_closure_hash:
if not kinematics_closure_path.is_file():
errors.append("kinematics source closure report is missing")
elif kinematics_closure_hash != sha256(kinematics_closure_path):
errors.append("kinematics source closure report hash does not match context lock")
if kinematics_closure_path.is_file() and isinstance(inventory_count, dict):
try:
kinematics_report = json.loads(kinematics_closure_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("kinematics source closure report is not valid JSON")
else:
expected_counts = {
"kinematics_source_files": kinematics_report.get("source", {}).get("implementation_file_count"),
"kinematics_config_entries": kinematics_report.get("configuration", {}).get("entry_count"),
"kinematics_hal_entries": kinematics_report.get("hal", {}).get("entry_count"),
"kinematics_test_files": kinematics_report.get("tests", {}).get("file_count"),
}
for field, value in expected_counts.items():
if value is not None and inventory_count.get(field) != value:
errors.append(f"{field} count does not match kinematics closure report")
kinematics_graph_path = repo_root / "coverage/kinematics-config-graph.report.json"
kinematics_graph_hash = lock.get("kinematics_config_graph_report_sha256")
if kinematics_graph_hash:
if not kinematics_graph_path.is_file():
errors.append("kinematics config graph report is missing")
elif kinematics_graph_hash != sha256(kinematics_graph_path):
errors.append("kinematics config graph report hash does not match context lock")
samples_path = repo_root / "coverage/kinematics-oracle-samples.jsonl"
samples_hash = lock.get("kinematics_oracle_samples_sha256")
if samples_hash:
if not samples_path.is_file():
errors.append("kinematics oracle samples are missing")
elif samples_hash != sha256(samples_path):
errors.append("kinematics oracle samples hash does not match context lock")
oracle_report_path = repo_root / "coverage/kinematics-oracle.report.json"
oracle_report_hash = lock.get("kinematics_oracle_report_sha256")
if oracle_report_hash:
if not oracle_report_path.is_file():
errors.append("kinematics oracle report is missing")
elif oracle_report_hash != sha256(oracle_report_path):
errors.append("kinematics oracle report hash does not match context lock")
oracle_schema_path = repo_root / "coverage/kinematics-golden-schema.json"
oracle_schema_hash = lock.get("kinematics_golden_schema_sha256")
if oracle_schema_hash:
if not oracle_schema_path.is_file():
errors.append("kinematics golden schema is missing")
elif oracle_schema_hash != sha256(oracle_schema_path):
errors.append("kinematics golden schema hash does not match context lock")
if kinematics_graph_path.is_file() and isinstance(inventory_count, dict):
try:
graph = json.loads(kinematics_graph_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("kinematics config graph report is not valid JSON")
else:
graph_counts = graph.get("counts", {})
for field, report_field in (("kinematics_graph_entries", "entry_nodes"), ("kinematics_graph_edges", "edges"), ("kinematics_oracle_samples", "samples")):
value = graph_counts.get(report_field)
if value is not None and inventory_count.get(field) != value:
errors.append(f"{field} count does not match kinematics config graph report")
if oracle_report_path.is_file() and isinstance(inventory_count, dict):
try:
oracle_report = json.loads(oracle_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("kinematics oracle report is not valid JSON")
else:
if oracle_report.get("sample_count") is not None and inventory_count.get("kinematics_oracle_samples") != oracle_report.get("sample_count"):
errors.append("kinematics oracle sample count does not match oracle report")
abi_contract_path = repo_root / "coverage/kinematics-abi-contract.json"
abi_contract_hash = lock.get("kinematics_abi_contract_sha256")
if abi_contract_hash:
if not abi_contract_path.is_file():
errors.append("kinematics ABI contract is missing")
elif abi_contract_hash != sha256(abi_contract_path):
errors.append("kinematics ABI contract hash does not match context lock")
if abi_contract_path.is_file():
try:
abi_contract = json.loads(abi_contract_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("kinematics ABI contract is not valid JSON")
else:
if abi_contract.get("status") != "verified":
errors.append("kinematics ABI contract is not verified")
mapping_contract_path = repo_root / "coverage/kinematics-mapping-contract.json"
mapping_contract_hash = lock.get("kinematics_mapping_contract_sha256")
if mapping_contract_hash:
if not mapping_contract_path.is_file():
errors.append("kinematics mapping contract is missing")
elif mapping_contract_hash != sha256(mapping_contract_path):
errors.append("kinematics mapping contract hash does not match context lock")
if mapping_contract_path.is_file():
try:
mapping_contract = json.loads(mapping_contract_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("kinematics mapping contract is not valid JSON")
else:
if mapping_contract.get("status") != "verified":
errors.append("kinematics mapping contract is not verified")
numeric_policy_path = repo_root / "coverage/kinematics-numeric-policy.json"
numeric_policy_hash = lock.get("kinematics_numeric_policy_sha256")
if numeric_policy_hash:
if not numeric_policy_path.is_file():
errors.append("kinematics numeric policy is missing")
elif numeric_policy_hash != sha256(numeric_policy_path):
errors.append("kinematics numeric policy hash does not match context lock")
if numeric_policy_path.is_file():
try:
numeric_policy = json.loads(numeric_policy_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("kinematics numeric policy is not valid JSON")
else:
if numeric_policy.get("status") != "verified":
errors.append("kinematics numeric policy is not verified")
trivkins_report_path = repo_root / "coverage/trivkins-integration.report.json"
trivkins_report_hash = lock.get("trivkins_integration_report_sha256")
if trivkins_report_hash:
if not trivkins_report_path.is_file():
errors.append("trivkins integration report is missing")
elif trivkins_report_hash != sha256(trivkins_report_path):
errors.append("trivkins integration report hash does not match context lock")
coupled_report_path = repo_root / "coverage/coupled-kinematics.report.json"
coupled_report_hash = lock.get("coupled_kinematics_report_sha256")
if coupled_report_hash:
if not coupled_report_path.is_file():
errors.append("coupled kinematics report is missing")
elif coupled_report_hash != sha256(coupled_report_path):
errors.append("coupled kinematics report hash does not match context lock")
if coupled_report_path.is_file():
try:
coupled_report = json.loads(coupled_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("coupled kinematics report is not valid JSON")
else:
if coupled_report.get("implementation_status") != "verified":
errors.append("coupled kinematics implementation is not verified")
if coupled_report.get("closure", {}).get("official_oracle") != "blocked":
errors.append("coupled kinematics oracle status must remain explicitly blocked")
corexy_hal_report_path = repo_root / "coverage/corexy-hal.report.json"
corexy_hal_report_hash = lock.get("corexy_hal_report_sha256")
if corexy_hal_report_hash:
if not corexy_hal_report_path.is_file():
errors.append("corexy HAL report is missing")
elif corexy_hal_report_hash != sha256(corexy_hal_report_path):
errors.append("corexy HAL report hash does not match context lock")
if corexy_hal_report_path.is_file():
try:
corexy_hal_report = json.loads(corexy_hal_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("corexy HAL report is not valid JSON")
else:
if corexy_hal_report.get("implementation_status") != "verified":
errors.append("corexy HAL implementation is not verified")
if corexy_hal_report.get("oracle", {}).get("status") != "blocked":
errors.append("corexy HAL oracle status must remain explicitly blocked")
fiveaxis_report_path = repo_root / "coverage/fiveaxis-kinematics.report.json"
fiveaxis_report_hash = lock.get("fiveaxis_kinematics_report_sha256")
if fiveaxis_report_hash:
if not fiveaxis_report_path.is_file():
errors.append("fiveaxis kinematics report is missing")
elif fiveaxis_report_hash != sha256(fiveaxis_report_path):
errors.append("fiveaxis kinematics report hash does not match context lock")
if fiveaxis_report_path.is_file():
try:
fiveaxis_report = json.loads(fiveaxis_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("fiveaxis kinematics report is not valid JSON")
else:
if fiveaxis_report.get("implementation_status") != "verified":
errors.append("fiveaxis kinematics implementation is not verified")
if fiveaxis_report.get("oracle", {}).get("status") != "blocked":
errors.append("fiveaxis kinematics oracle status must remain explicitly blocked")
trt_report_path = repo_root / "coverage/trt-kinematics.report.json"
trt_report_hash = lock.get("trt_kinematics_report_sha256")
if trt_report_hash:
if not trt_report_path.is_file():
errors.append("TRT kinematics report is missing")
elif trt_report_hash != sha256(trt_report_path):
errors.append("TRT kinematics report hash does not match context lock")
if trt_report_path.is_file():
try:
trt_report = json.loads(trt_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("TRT kinematics report is not valid JSON")
else:
if trt_report.get("implementation_status") != "verified":
errors.append("TRT kinematics implementation is not verified")
if trt_report.get("oracle", {}).get("status") != "blocked":
errors.append("TRT kinematics oracle status must remain explicitly blocked")
robot_report_path = repo_root / "coverage/robot-kinematics.report.json"
robot_report_hash = lock.get("robot_kinematics_report_sha256")
if robot_report_hash:
if not robot_report_path.is_file():
errors.append("robot kinematics report is missing")
elif robot_report_hash != sha256(robot_report_path):
errors.append("robot kinematics report hash does not match context lock")
if robot_report_path.is_file():
try:
robot_report = json.loads(robot_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("robot kinematics report is not valid JSON")
else:
if robot_report.get("implementation_status") != "verified":
errors.append("robot kinematics implementation is not verified")
if robot_report.get("oracle", {}).get("status") != "blocked":
errors.append("robot kinematics oracle status must remain explicitly blocked")
genser_report_path = repo_root / "coverage/genser-kinematics.report.json"
genser_report_hash = lock.get("genser_kinematics_report_sha256")
if genser_report_hash:
if not genser_report_path.is_file():
errors.append("genser kinematics report is missing")
elif genser_report_hash != sha256(genser_report_path):
errors.append("genser kinematics report hash does not match context lock")
if genser_report_path.is_file():
try:
genser_report = json.loads(genser_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("genser kinematics report is not valid JSON")
else:
if genser_report.get("implementation_status") != "verified":
errors.append("genser kinematics implementation is not verified")
if genser_report.get("oracle", {}).get("status") != "blocked":
errors.append("genser kinematics oracle status must remain explicitly blocked")
delta_report_path = repo_root / "coverage/delta-kinematics.report.json"
delta_report_hash = lock.get("delta_kinematics_report_sha256")
if delta_report_hash:
if not delta_report_path.is_file():
errors.append("delta kinematics report is missing")
elif delta_report_hash != sha256(delta_report_path):
errors.append("delta kinematics report hash does not match context lock")
if delta_report_path.is_file():
try:
delta_report = json.loads(delta_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("delta kinematics report is not valid JSON")
else:
if delta_report.get("implementation_status") != "verified":
errors.append("delta kinematics implementation is not verified")
if delta_report.get("oracle", {}).get("status") != "blocked":
errors.append("delta kinematics oracle status must remain explicitly blocked")
tripod_report_path = repo_root / "coverage/tripod-kinematics.report.json"
tripod_report_hash = lock.get("tripod_kinematics_report_sha256")
if tripod_report_hash:
if not tripod_report_path.is_file():
errors.append("tripod kinematics report is missing")
elif tripod_report_hash != sha256(tripod_report_path):
errors.append("tripod kinematics report hash does not match context lock")
if tripod_report_path.is_file():
try:
tripod_report = json.loads(tripod_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("tripod kinematics report is not valid JSON")
else:
if tripod_report.get("implementation_status") != "verified":
errors.append("tripod kinematics implementation is not verified")
if tripod_report.get("oracle", {}).get("status") != "blocked":
errors.append("tripod kinematics oracle status must remain explicitly blocked")
genhex_report_path = repo_root / "coverage/genhex-kinematics.report.json"
genhex_report_hash = lock.get("genhex_kinematics_report_sha256")
if genhex_report_hash:
if not genhex_report_path.is_file():
errors.append("genhex kinematics report is missing")
elif genhex_report_hash != sha256(genhex_report_path):
errors.append("genhex kinematics report hash does not match context lock")
if genhex_report_path.is_file():
try:
genhex_report = json.loads(genhex_report_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
errors.append("genhex kinematics report is not valid JSON")
else:
if genhex_report.get("implementation_status") != "verified":
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")
source_ids = [str(row.get("source_id", "")) for row in sources] source_ids = [str(row.get("source_id", "")) for row in sources]
if len(source_ids) != len(set(source_ids)): if len(source_ids) != len(set(source_ids)):
errors.append("source index contains duplicate source_id") errors.append("source index contains duplicate source_id")

View File

@@ -0,0 +1,501 @@
#!/usr/bin/env python3
"""Check that a bounded context packet leaves room for model output.
This is deliberately provider-neutral. The caller supplies the model's
context window; the packet's input budget and a fixed output reserve are
checked before an API request is made.
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from typing import Any
OUTPUT_TOKEN_RESERVE = {
"doc": 768,
"feature": 1024,
"contract": 1536,
"milestone": 2048,
}
PROFILE_INPUT_TOKEN_LIMIT = {
"doc": 2048,
"feature": 4096,
"contract": 6144,
"milestone": 8192,
}
PROFILE_BYTE_LIMIT = {
"doc": 8192,
"feature": 16384,
"contract": 24576,
"milestone": 32768,
}
PROFILE_EVIDENCE_BYTE_LIMIT = {
"doc": 2048,
"feature": 6144,
"contract": 10240,
"milestone": 14336,
}
PROFILE_EVIDENCE_FILE_LIMIT = {
"doc": 8,
"feature": 16,
"contract": 24,
"milestone": 32,
}
DEFAULT_CONTEXT_WINDOW = 16384
DEFAULT_SAFETY_MARGIN = 256
# Without a provider tokenizer, UTF-8 bytes are the only portable hard upper
# bound. Any multi-byte-per-token heuristic can under-count provider tokens
# for punctuation-heavy or non-ASCII input and is unsafe for a request gate.
TOKEN_BYTES_PER_TOKEN = 1
PACKET_FIELDS = {
"context_id",
"packet_id",
"task",
"header",
"baseline",
"scope",
"contracts",
"evidence",
"risks",
"next",
"forbidden",
"budget",
}
FORBIDDEN_CONTEXT_FIELDS = {
"history",
"chat",
"conversation",
"messages",
"transcript",
"previous_packet",
"prior_context",
}
EXCLUSION_REASON_CODES = {"E1", "E2", "E3", "E4", "E5", "E6"}
class ModelBudgetError(ValueError):
"""Raised when a request cannot fit input, output, and safety budgets."""
def _response_object(value: Any) -> dict[str, Any]:
"""Return a provider response object from a response or SSE envelope.
The Responses API exposes usage on the response itself, while streaming
``response.completed`` events wrap it below a ``response`` key. Keeping
this normalization here prevents callers from accidentally validating the
event envelope instead of the actual provider usage.
"""
if not isinstance(value, dict):
raise ModelBudgetError("provider response must be an object")
response = value.get("response")
if isinstance(response, dict):
return response
return value
def extract_response_usage(value: Any) -> dict[str, int]:
"""Extract authoritative token usage from a provider response envelope."""
response = _response_object(value)
usage = response.get("usage")
if not isinstance(usage, dict):
raise ModelBudgetError("provider response is missing usage")
try:
input_tokens = int(usage["input_tokens"])
output_tokens = int(usage["output_tokens"])
total_tokens = int(usage.get("total_tokens", input_tokens + output_tokens))
except (KeyError, TypeError, ValueError) as exc:
raise ModelBudgetError("provider usage has invalid token counts") from exc
if min(input_tokens, output_tokens, total_tokens) < 0:
raise ModelBudgetError("provider usage token counts must be non-negative")
if total_tokens < input_tokens + output_tokens:
raise ModelBudgetError("provider usage total is smaller than input plus output")
return {
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": total_tokens,
}
def _find_forbidden(value: Any, path: str = "") -> str | None:
"""Find accumulated conversation state, including when nested."""
if isinstance(value, dict):
for key, child in value.items():
key_text = str(key)
child_path = f"{path}.{key_text}" if path else key_text
if key_text in FORBIDDEN_CONTEXT_FIELDS:
return child_path
found = _find_forbidden(child, child_path)
if found:
return found
elif isinstance(value, list):
for index, child in enumerate(value):
found = _find_forbidden(child, f"{path}[{index}]")
if found:
return found
return None
def _packet_input_bytes(packet: dict[str, Any]) -> int:
budget = packet.get("budget")
if not isinstance(budget, dict):
raise ModelBudgetError("packet budget is missing")
try:
packet_bytes = int(budget["packet_bytes"])
evidence_bytes = int(budget["selected_evidence_bytes"])
except (KeyError, TypeError, ValueError) as exc:
raise ModelBudgetError("packet budget lacks input byte totals") from exc
if packet_bytes < 0 or evidence_bytes < 0:
raise ModelBudgetError("packet input byte totals must be non-negative")
return packet_bytes + evidence_bytes
def _canonical_packet_bytes(packet: dict[str, Any]) -> int:
"""Return the exact UTF-8 size used by the packet generator."""
return len(
json.dumps(packet, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
)
def _validate_packet_metadata(packet: dict[str, Any], budget: dict[str, Any]) -> None:
"""Reject stale or inconsistent budget metadata before a model request.
Small unit-test fixtures may contain only a budget object. Full packets
generated for a request always carry ``context_id``/``packet_id`` and an
evidence envelope, so strict consistency checks are applied to those.
"""
forbidden = _find_forbidden(packet)
if forbidden:
raise ModelBudgetError(
"packet contains forbidden accumulated context "
f"field: {forbidden}"
)
# Generated packets have a closed envelope. Reject ad-hoc top-level
# fields that could smuggle full logs, tool output or a previous request
# beside the audited eight segments.
if "budget" in packet:
unknown = sorted(set(packet) - PACKET_FIELDS)
if unknown:
raise ModelBudgetError(
"packet contains unknown top-level context field: "
f"{unknown[0]}"
)
if set(packet) != {"budget"}:
actual = _canonical_packet_bytes(packet)
declared = int(budget["packet_bytes"])
if declared != actual:
raise ModelBudgetError(
"packet byte metadata does not match serialized packet "
f"(declared={declared}, actual={actual})"
)
evidence = packet.get("evidence")
if not isinstance(evidence, dict):
return
selected = evidence.get("selected", [])
excluded = evidence.get("excluded", [])
if not isinstance(selected, list) or not isinstance(excluded, list):
raise ModelBudgetError("packet evidence must contain selected/excluded lists")
for item in excluded:
if not isinstance(item, dict) or item.get("reason_code") not in EXCLUSION_REASON_CODES:
raise ModelBudgetError("excluded evidence has an unknown reason code")
selected_bytes = 0
for item in selected:
if not isinstance(item, dict):
raise ModelBudgetError("selected evidence entries must be objects")
try:
item_bytes = int(item["bytes"])
except (KeyError, TypeError, ValueError) as exc:
raise ModelBudgetError("selected evidence entry lacks bytes") from exc
if item_bytes < 0:
raise ModelBudgetError("selected evidence bytes must be non-negative")
selected_bytes += item_bytes
if selected_bytes != int(budget["selected_evidence_bytes"]):
raise ModelBudgetError(
"selected evidence byte metadata does not match entries "
f"(declared={budget['selected_evidence_bytes']}, actual={selected_bytes})"
)
if "selected_evidence_files" in budget and int(budget["selected_evidence_files"]) != len(selected):
raise ModelBudgetError("selected evidence file count does not match entries")
if "excluded_evidence_files" in budget and int(budget["excluded_evidence_files"]) != len(excluded):
raise ModelBudgetError("excluded evidence file count does not match entries")
if "evidence_byte_limit" in budget and selected_bytes > int(budget["evidence_byte_limit"]):
raise ModelBudgetError("selected evidence exceeds its evidence byte budget")
if "evidence_file_limit" in budget and len(selected) > int(budget["evidence_file_limit"]):
raise ModelBudgetError("selected evidence exceeds its file budget")
def estimate_input_tokens(packet: dict[str, Any]) -> int:
"""Return a safe provider-neutral upper bound from UTF-8 bytes.
A tokenizer supplied by the API client may replace this estimate. The
byte-for-token bound is deliberately strict: it may reject a packet that
would fit a particular provider, but it cannot silently approve a packet
that that provider tokenizes larger than expected.
"""
return math.ceil(_packet_input_bytes(packet) / TOKEN_BYTES_PER_TOKEN)
def validate_request(
packet: dict[str, Any],
*,
context_window: int = DEFAULT_CONTEXT_WINDOW,
max_output_tokens: int | None = None,
safety_margin: int = DEFAULT_SAFETY_MARGIN,
input_tokens: int | None = None,
overhead_tokens: int = 0,
) -> dict[str, int]:
budget = packet.get("budget")
if not isinstance(budget, dict):
raise ModelBudgetError("packet budget is missing")
try:
input_limit = int(budget["token_limit"])
byte_limit = int(budget["byte_limit"])
packet_bytes = int(budget["packet_bytes"])
selected_evidence_bytes = int(budget["selected_evidence_bytes"])
except (KeyError, TypeError, ValueError) as exc:
raise ModelBudgetError("packet budget lacks required fields") from exc
profile = str(budget.get("profile", ""))
try:
profile_output_limit = OUTPUT_TOKEN_RESERVE[profile]
profile_input_limit = PROFILE_INPUT_TOKEN_LIMIT[profile]
profile_byte_limit = PROFILE_BYTE_LIMIT[profile]
profile_evidence_limit = PROFILE_EVIDENCE_BYTE_LIMIT[profile]
profile_file_limit = PROFILE_EVIDENCE_FILE_LIMIT[profile]
except KeyError as exc:
raise ModelBudgetError(f"unknown packet profile: {profile}") from exc
if max_output_tokens is None:
max_output_tokens = profile_output_limit
try:
context_window = int(context_window)
max_output_tokens = int(max_output_tokens)
safety_margin = int(safety_margin)
overhead_tokens = int(overhead_tokens)
declared_evidence_limit = int(budget.get("evidence_byte_limit", profile_evidence_limit))
declared_file_limit = int(budget.get("evidence_file_limit", profile_file_limit))
except (TypeError, ValueError) as exc:
raise ModelBudgetError("model budget fields must be integers") from exc
values = (
context_window,
max_output_tokens,
safety_margin,
input_limit,
byte_limit,
packet_bytes,
selected_evidence_bytes,
overhead_tokens,
declared_evidence_limit,
declared_file_limit,
)
if any(value < 0 for value in values):
raise ModelBudgetError("model budgets must be non-negative")
if input_limit > profile_input_limit:
raise ModelBudgetError(
"packet token limit exceeds profile "
f"(declared={input_limit}, profile={profile_input_limit})"
)
if byte_limit > profile_byte_limit:
raise ModelBudgetError(
"packet byte limit exceeds profile "
f"(declared={byte_limit}, profile={profile_byte_limit})"
)
if declared_evidence_limit > profile_evidence_limit:
raise ModelBudgetError(
"evidence byte limit exceeds profile "
f"(declared={declared_evidence_limit}, profile={profile_evidence_limit})"
)
if declared_file_limit > profile_file_limit:
raise ModelBudgetError(
"evidence file limit exceeds profile "
f"(declared={declared_file_limit}, profile={profile_file_limit})"
)
if max_output_tokens > profile_output_limit:
raise ModelBudgetError(
"output token budget exceeds profile "
f"(requested={max_output_tokens}, profile={profile_output_limit})"
)
try:
_validate_packet_metadata(packet, budget)
except ModelBudgetError:
raise
except (KeyError, TypeError, ValueError) as exc:
raise ModelBudgetError("packet budget metadata is malformed") from exc
context_bytes = packet_bytes + selected_evidence_bytes
if packet_bytes > byte_limit or context_bytes > byte_limit:
raise ModelBudgetError("packet exceeds its declared byte budget")
if selected_evidence_bytes > declared_evidence_limit:
raise ModelBudgetError("selected evidence exceeds its evidence byte budget")
if selected_evidence_bytes > profile_evidence_limit:
raise ModelBudgetError("selected evidence exceeds profile byte budget")
try:
estimated_input = estimate_input_tokens(packet) if input_tokens is None else int(input_tokens)
except (TypeError, ValueError) as exc:
raise ModelBudgetError("input token count must be an integer") from exc
if estimated_input < 0:
raise ModelBudgetError("input token count must be non-negative")
if estimated_input > input_limit:
raise ModelBudgetError(
"packet input token budget exceeded "
f"(estimated={estimated_input}, limit={input_limit})"
)
# Reserve the input actually sent for this request plus fixed provider
# overhead. Reserving the full profile here rejects small packets for no
# reason, while the packet's own input_limit still prevents later growth.
effective_input = estimated_input + overhead_tokens
available = context_window - safety_margin - max_output_tokens
if effective_input > available:
raise ModelBudgetError(
"model context budget exceeded "
f"(input={effective_input}, output={max_output_tokens}, "
f"safety={safety_margin}, window={context_window})"
)
return {
"context_window": context_window,
"input_token_limit": input_limit,
"estimated_input_tokens": estimated_input,
"overhead_tokens": overhead_tokens,
"estimated_request_input_tokens": estimated_input + overhead_tokens,
"max_output_tokens": max_output_tokens,
"safety_margin": safety_margin,
"reserved_total_tokens": effective_input + max_output_tokens + safety_margin,
"available_input_tokens": max(0, available - overhead_tokens),
}
def validate_response(
output_tokens: int,
*,
max_output_tokens: int,
finish_reason: str | None = None,
) -> dict[str, int | str | None]:
"""Validate the completion actually returned by a provider.
The preflight check protects the request, while this postflight check
catches SDKs that ignore or reinterpret ``max_output_tokens``. A length
finish is reported as truncation so callers can persist a checkpoint and
continue with a fresh bounded request instead of replaying the old one.
"""
try:
actual = int(output_tokens)
limit = int(max_output_tokens)
except (TypeError, ValueError) as exc:
raise ModelBudgetError("response token counts must be integers") from exc
if actual < 0 or limit < 0:
raise ModelBudgetError("response token counts must be non-negative")
if actual > limit:
raise ModelBudgetError(
"model response exceeded output token budget "
f"(actual={actual}, limit={limit})"
)
return {
"output_tokens": actual,
"max_output_tokens": limit,
"finish_reason": finish_reason,
"truncated": finish_reason in {
"length",
"max_tokens",
"max_output_tokens",
"max_completion_tokens",
},
}
def validate_provider_response(
response: Any,
*,
max_output_tokens: int,
) -> dict[str, int | str | None | bool]:
"""Validate usage and completion state returned by a provider.
``response`` may be a normal Responses API object or a streaming event
envelope. Failed/incomplete responses are rejected as non-reusable so a
caller cannot append partial output to the next request. A length-limit
completion is returned as ``truncated`` and must be resumed from a fresh
checkpoint.
"""
response_object = _response_object(response)
usage = extract_response_usage(response_object)
status = response_object.get("status")
incomplete = response_object.get("incomplete_details")
finish_reason = None
if isinstance(incomplete, dict):
reason = incomplete.get("reason")
if reason is not None:
finish_reason = str(reason)
if finish_reason is None and response_object.get("finish_reason") is not None:
finish_reason = str(response_object["finish_reason"])
if status is None and finish_reason is None:
raise ModelBudgetError("provider response is missing completion status")
result = validate_response(
usage["output_tokens"],
max_output_tokens=max_output_tokens,
finish_reason=finish_reason,
)
if status in {"failed", "cancelled"}:
raise ModelBudgetError(
f"provider response is not reusable (status={status})"
)
if status == "incomplete" and not result["truncated"]:
raise ModelBudgetError(
"provider response is incomplete without a length checkpoint reason"
)
result.update(usage)
result["status"] = str(status) if status is not None else None
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("packet", type=Path)
parser.add_argument("--context-window", type=int, default=DEFAULT_CONTEXT_WINDOW)
parser.add_argument("--max-output-tokens", type=int)
parser.add_argument("--safety-margin", type=int, default=DEFAULT_SAFETY_MARGIN)
parser.add_argument("--input-tokens", type=int, help="exact count from the provider tokenizer")
parser.add_argument(
"--overhead-tokens",
type=int,
required=True,
help="system/tool/history tokens outside the context packet",
)
args = parser.parse_args()
try:
with args.packet.open("r", encoding="utf-8") as handle:
packet = json.load(handle)
if not isinstance(packet, dict):
raise ModelBudgetError("packet must be a JSON object")
result = validate_request(
packet,
context_window=args.context_window,
max_output_tokens=args.max_output_tokens,
safety_margin=args.safety_margin,
input_tokens=args.input_tokens,
overhead_tokens=args.overhead_tokens,
)
except (OSError, UnicodeError, json.JSONDecodeError, ModelBudgetError) as exc:
parser.error(str(exc))
print(
"model_budget=valid "
f"input_tokens={result['input_token_limit']} "
f"estimated_input={result['estimated_input_tokens']} "
f"overhead={result['overhead_tokens']} "
f"max_output={result['max_output_tokens']} "
f"reserved_total={result['reserved_total_tokens']}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Validate the small task pointer loaded at the start of a work session."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
MAX_POINTER_BYTES = 8192
MAX_POINTER_LINES = 180
REQUIRED_HEADINGS = ("## Current", "## Read", "## Task", "## Next", "## Verify", "## Stop")
FORBIDDEN_HEADINGS = (
"## 6. 已验证证据",
"## Evidence",
"## History",
"## 历史",
"## Chat",
)
class PointerError(ValueError):
"""Raised when the bootstrap pointer would expand the model context."""
def validate_pointer(
path: Path,
*,
max_bytes: int = MAX_POINTER_BYTES,
max_lines: int = MAX_POINTER_LINES,
lock_path: Path | None = None,
) -> dict[str, int]:
text = path.read_text(encoding="utf-8")
size = len(text.encode("utf-8"))
lines = len(text.splitlines())
if size > max_bytes:
raise PointerError(f"task pointer exceeds byte budget ({size}>{max_bytes})")
if lines > max_lines:
raise PointerError(f"task pointer exceeds line budget ({lines}>{max_lines})")
missing = [heading for heading in REQUIRED_HEADINGS if heading not in text]
if missing:
raise PointerError(f"task pointer missing sections: {', '.join(missing)}")
found = [heading for heading in FORBIDDEN_HEADINGS if re.search(rf"^\s*{re.escape(heading)}\s*$", text, re.MULTILINE)]
if found:
raise PointerError(f"task pointer contains history section: {found[0]}")
if "coverage/" not in text:
raise PointerError("task pointer must direct large evidence to paths")
pointer_match = re.search(r"^context_id:\s*([^\s]+)\s*$", text, re.MULTILINE)
if pointer_match is None:
raise PointerError("task pointer is missing context_id")
if lock_path is not None and lock_path.exists():
try:
lock = json.loads(lock_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise PointerError(f"cannot read context lock: {exc}") from exc
lock_id = lock.get("context_id") if isinstance(lock, dict) else None
if not lock_id:
raise PointerError("context lock is missing context_id")
if pointer_match.group(1) != str(lock_id):
raise PointerError(
f"task pointer context_id does not match lock ({pointer_match.group(1)}!={lock_id})"
)
return {"bytes": size, "lines": lines, "max_bytes": max_bytes, "max_lines": max_lines}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("path", nargs="?", type=Path, default=Path("nextTask.md"))
parser.add_argument("--max-bytes", type=int, default=MAX_POINTER_BYTES)
parser.add_argument("--max-lines", type=int, default=MAX_POINTER_LINES)
parser.add_argument("--lock", type=Path, default=Path("context-lock.json"))
args = parser.parse_args()
try:
result = validate_pointer(
args.path,
max_bytes=args.max_bytes,
max_lines=args.max_lines,
lock_path=args.lock,
)
except (OSError, UnicodeError, PointerError) as exc:
parser.error(str(exc))
print(f"task_pointer=valid bytes={result['bytes']} lines={result['lines']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -6,7 +6,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(sim_core STATIC src/sim_api.c src/sim_protocol.c src/sim_event.c src/sim_hal.c src/sim_posemath.c) add_library(sim_core STATIC src/sim_api.c src/sim_protocol.c src/sim_event.c src/sim_hal.c src/sim_posemath.c src/sim_interpreter.c src/sim_motion_queue.c src/sim_kinematics.c)
target_include_directories(sim_core PUBLIC include) target_include_directories(sim_core PUBLIC include)
target_compile_definitions(sim_core PRIVATE SIM_API_BUILD=1) target_compile_definitions(sim_core PRIVATE SIM_API_BUILD=1)
@@ -22,7 +22,9 @@ target_include_directories(sim_posemath PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/libnml/posemath ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/libnml/posemath
) )
target_compile_definitions(sim_posemath PRIVATE ULAPI) target_compile_definitions(sim_posemath PRIVATE ULAPI)
target_compile_options(sim_posemath PRIVATE -ffp-contract=off -fno-fast-math)
target_link_libraries(sim_core PUBLIC sim_posemath) target_link_libraries(sim_core PUBLIC sim_posemath)
target_compile_options(sim_core PRIVATE -ffp-contract=off -fno-fast-math)
if(NOT EMSCRIPTEN) if(NOT EMSCRIPTEN)
add_executable(sim_api_test tests/sim_api_test.c) add_executable(sim_api_test tests/sim_api_test.c)
@@ -42,29 +44,59 @@ if(NOT EMSCRIPTEN)
target_include_directories(sim_event_test PRIVATE include) target_include_directories(sim_event_test PRIVATE include)
add_test(NAME sim_event_test COMMAND sim_event_test) add_test(NAME sim_event_test COMMAND sim_event_test)
add_executable(sim_hal_test tests/sim_hal_test.c) add_executable(sim_hal_test tests/sim_hal_test.c tests/sim_hal_test_main.c)
target_link_libraries(sim_hal_test PRIVATE sim_core) target_link_libraries(sim_hal_test PRIVATE sim_core)
target_include_directories(sim_hal_test PRIVATE include) target_include_directories(sim_hal_test PRIVATE include)
target_compile_options(sim_hal_test PRIVATE -UNDEBUG)
add_test(NAME sim_hal_test COMMAND sim_hal_test) add_test(NAME sim_hal_test COMMAND sim_hal_test)
add_executable(sim_posemath_test tests/sim_posemath_test.c) add_executable(sim_posemath_test tests/sim_posemath_test.c)
target_link_libraries(sim_posemath_test PRIVATE sim_core) target_link_libraries(sim_posemath_test PRIVATE sim_core)
target_include_directories(sim_posemath_test PRIVATE include) target_include_directories(sim_posemath_test PRIVATE include)
add_test(NAME sim_posemath_test COMMAND sim_posemath_test) add_test(NAME sim_posemath_test COMMAND sim_posemath_test)
add_executable(sim_interpreter_test tests/sim_interpreter_test.c)
target_link_libraries(sim_interpreter_test PRIVATE sim_core)
target_include_directories(sim_interpreter_test PRIVATE include)
add_test(NAME sim_interpreter_test COMMAND sim_interpreter_test)
add_executable(sim_motion_queue_test tests/sim_motion_queue_test.c)
target_link_libraries(sim_motion_queue_test PRIVATE sim_core)
target_include_directories(sim_motion_queue_test PRIVATE include)
add_test(NAME sim_motion_queue_test COMMAND sim_motion_queue_test)
add_executable(sim_kinematics_test tests/sim_kinematics_test.c tests/sim_kinematics_test_main.c)
target_link_libraries(sim_kinematics_test PRIVATE sim_core)
target_include_directories(sim_kinematics_test PRIVATE include)
target_compile_options(sim_kinematics_test PRIVATE -UNDEBUG)
add_test(NAME sim_kinematics_test COMMAND sim_kinematics_test)
endif() endif()
if(EMSCRIPTEN) if(EMSCRIPTEN)
add_executable(sim_core_module src/sim_api.c src/sim_protocol.c src/sim_event.c src/sim_hal.c src/sim_posemath.c) add_executable(sim_hal_wasm_test tests/sim_hal_test.c tests/sim_hal_test_main.c)
target_link_libraries(sim_hal_wasm_test PRIVATE sim_core)
target_include_directories(sim_hal_wasm_test PRIVATE include)
target_compile_options(sim_hal_wasm_test PRIVATE -UNDEBUG)
target_link_options(sim_hal_wasm_test PRIVATE -sSINGLE_FILE=1)
add_executable(sim_kinematics_wasm_test tests/sim_kinematics_test.c tests/sim_kinematics_test_main.c)
target_link_libraries(sim_kinematics_wasm_test PRIVATE sim_core)
target_include_directories(sim_kinematics_wasm_test PRIVATE include)
target_compile_options(sim_kinematics_wasm_test PRIVATE -UNDEBUG)
target_link_options(sim_kinematics_wasm_test PRIVATE -sSINGLE_FILE=1)
add_executable(sim_core_module src/sim_api.c src/sim_protocol.c src/sim_event.c src/sim_hal.c src/sim_posemath.c src/sim_interpreter.c src/sim_motion_queue.c src/sim_kinematics.c)
target_include_directories(sim_core_module PRIVATE target_include_directories(sim_core_module PRIVATE
include include
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/rtapi ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/rtapi
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/libnml/posemath ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/libnml/posemath
) )
target_compile_options(sim_core_module PRIVATE -ffp-contract=off -fno-fast-math)
target_link_libraries(sim_core_module PRIVATE sim_posemath) target_link_libraries(sim_core_module PRIVATE sim_posemath)
set_target_properties(sim_core_module PROPERTIES set_target_properties(sim_core_module PROPERTIES
OUTPUT_NAME "linuxcnc-sim-core" OUTPUT_NAME "linuxcnc-sim-core"
SUFFIX ".js" SUFFIX ".js"
LINK_FLAGS "-sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=web,worker -sEXPORTED_FUNCTIONS=['_malloc','_free','_sim_create','_sim_load_workspace','_sim_command','_sim_step','_sim_read_snapshot','_sim_read_event','_sim_export_state','_sim_import_state','_sim_destroy','_sim_protocol_reset','_sim_protocol_accept','_sim_protocol_pause','_sim_protocol_resume','_sim_protocol_state_value','_sim_protocol_last_seq','_sim_protocol_tick','_sim_protocol_set_tick','_sim_event_reset','_sim_event_append','_sim_event_count','_sim_event_cursor','_sim_event_read','_sim_event_export','_sim_event_import','_sim_hal_reset','_sim_hal_register_pin','_sim_hal_set_pin','_sim_hal_get_pin','_sim_hal_connect','_sim_hal_register_thread','_sim_hal_register_function','_sim_hal_set_function_failure','_sim_hal_step','_sim_hal_tick','_sim_hal_function_calls','_sim_hal_pin_count','_sim_hal_thread_count','_sim_hal_function_count','_sim_posemath_normalize','_sim_posemath_dot','_sim_posemath_line_point','_sim_posemath_is_finite'] -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']" LINK_FLAGS "-sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=web,worker -sEXPORTED_FUNCTIONS=['_malloc','_free','_sim_create','_sim_load_workspace','_sim_command','_sim_step','_sim_read_snapshot','_sim_read_event','_sim_export_state','_sim_import_state','_sim_destroy','_sim_protocol_reset','_sim_protocol_accept','_sim_protocol_pause','_sim_protocol_resume','_sim_protocol_state_value','_sim_protocol_last_seq','_sim_protocol_tick','_sim_protocol_set_tick','_sim_event_reset','_sim_event_append','_sim_event_count','_sim_event_cursor','_sim_event_read','_sim_event_export','_sim_event_import','_sim_hal_reset','_sim_hal_register_pin','_sim_hal_set_pin','_sim_hal_get_pin','_sim_hal_connect','_sim_hal_register_thread','_sim_hal_register_function','_sim_hal_set_function_failure','_sim_hal_step','_sim_hal_tick','_sim_hal_function_calls','_sim_hal_pin_count','_sim_hal_thread_count','_sim_hal_function_count','_sim_posemath_normalize','_sim_posemath_dot','_sim_posemath_line_point','_sim_posemath_is_finite','_sim_posemath_pose_is_finite','_sim_posemath_pose_line_point','_sim_posemath_segment_validate','_sim_posemath_segment_point','_sim_posemath_hash_state','_sim_tp_init','_sim_tp_reset','_sim_tp_enqueue','_sim_tp_step','_sim_tp_read_state','_sim_tp_state_hash','_sim_interpreter_init','_sim_interpreter_reset','_sim_interpreter_execute','_sim_interpreter_read_state','_sim_interpreter_canonical','_sim_interpreter_state_hash','_sim_interpreter_block_hash','_sim_motion_init','_sim_motion_reset','_sim_motion_enqueue','_sim_motion_step','_sim_motion_read_state','_sim_motion_queue_depth','_sim_motion_state_hash','_sim_motion_canonical','_sim_kinematics_init','_sim_kinematics_forward','_sim_kinematics_forward_ex','_sim_kinematics_inverse','_sim_kinematics_inverse_ex','_sim_kinematics_home','_sim_kinematics_validate_pose','_sim_kinematics_state_hash'] -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']"
) )
endif() endif()

View File

@@ -49,6 +49,10 @@ int32_t sim_hal_get_pin(const char *name, sim_hal_value *out);
int32_t sim_hal_connect(const char *source, const char *target); int32_t sim_hal_connect(const char *source, const char *target);
int32_t sim_hal_register_thread(const char *name, uint32_t period_ticks); int32_t sim_hal_register_thread(const char *name, uint32_t period_ticks);
int32_t sim_hal_register_function(const char *thread, const char *name); int32_t sim_hal_register_function(const char *thread, const char *name);
/* Register the static LinuxCNC corexy_by_hal component and its servo pins. */
int32_t sim_hal_register_corexy_by_hal(const char *component, const char *thread);
/* Execute one deterministic CoreXY command/feedback transform immediately. */
int32_t sim_hal_corexy_transform(const char *component);
int32_t sim_hal_set_function_failure(const char *name, uint32_t fail); int32_t sim_hal_set_function_failure(const char *name, uint32_t fail);
int32_t sim_hal_step(uint32_t ticks); int32_t sim_hal_step(uint32_t ticks);
uint32_t sim_hal_tick(void); uint32_t sim_hal_tick(void);

View File

@@ -0,0 +1,74 @@
#ifndef LINUXCNC_WEB_WASM_SIM_INTERPRETER_H
#define LINUXCNC_WEB_WASM_SIM_INTERPRETER_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
enum sim_interpreter_result {
SIM_INTERP_OK = 0,
SIM_INTERP_ERR_ARGUMENT = -500,
SIM_INTERP_ERR_NONFINITE = -501,
SIM_INTERP_ERR_SYNTAX = -502,
SIM_INTERP_ERR_RANGE = -503,
SIM_INTERP_ERR_UNKNOWN_CODE = -504,
SIM_INTERP_ERR_STATE = -505,
SIM_INTERP_ERR_BUFFER = -506,
};
enum sim_interpreter_units {
SIM_INTERP_UNITS_INCH = 20,
SIM_INTERP_UNITS_MM = 21,
};
enum sim_interpreter_distance_mode {
SIM_INTERP_ABSOLUTE = 90,
SIM_INTERP_INCREMENTAL = 91,
};
typedef struct sim_interpreter_state {
double x;
double y;
double z;
double a;
double b;
double c;
double u;
double v;
double w;
double feed;
double speed;
uint32_t line;
int units;
int distance_mode;
int plane;
int motion_mode;
int spindle_mode;
int program_end;
int status;
} sim_interpreter_state;
#define SIM_INTERPRETER_CANONICAL_CAPACITY 512u
typedef struct sim_interpreter {
sim_interpreter_state state;
uint64_t block_hash;
uint32_t canonical_length;
char canonical[SIM_INTERPRETER_CANONICAL_CAPACITY];
} sim_interpreter;
int sim_interpreter_init(sim_interpreter *interpreter);
int sim_interpreter_reset(sim_interpreter *interpreter);
int sim_interpreter_execute(sim_interpreter *interpreter, const char *block, uint32_t length);
int sim_interpreter_read_state(const sim_interpreter *interpreter, sim_interpreter_state *state);
uint32_t sim_interpreter_canonical(const sim_interpreter *interpreter, char *output, uint32_t capacity);
uint64_t sim_interpreter_state_hash(const sim_interpreter *interpreter);
uint64_t sim_interpreter_block_hash(const sim_interpreter *interpreter);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,174 @@
#ifndef LINUXCNC_WEB_WASM_SIM_KINEMATICS_H
#define LINUXCNC_WEB_WASM_SIM_KINEMATICS_H
#include "sim_posemath.h"
#ifdef __cplusplus
extern "C" {
#endif
enum sim_kinematics_result {
SIM_KIN_OK = 0,
SIM_KIN_ERR_ARGUMENT = -700,
SIM_KIN_ERR_NONFINITE = -701,
SIM_KIN_ERR_RANGE = -702,
SIM_KIN_ERR_CONFIGURATION = -703,
SIM_KIN_ERR_SINGULAR = -704,
SIM_KIN_ERR_UNKNOWN_TYPE = -705,
SIM_KIN_ERR_UNSUPPORTED = -706,
};
enum sim_kinematics_type {
SIM_KIN_TRIVKINS = 1,
SIM_KIN_XYZAC_TRT = 2,
SIM_KIN_XYZBC_TRT = 3,
SIM_KIN_XYZBCW = 4,
SIM_KIN_COREXY = 5,
SIM_KIN_ROSE = 6,
SIM_KIN_MAXKINS = 7,
SIM_KIN_ROTATEKINS = 8,
SIM_KIN_5AXISKINS = 9,
SIM_KIN_SCARA = 10,
SIM_KIN_PUMA = 11,
SIM_KIN_SCORBOT = 12,
SIM_KIN_GENSER = 13,
SIM_KIN_LINEAR_DELTA = 14,
SIM_KIN_ROTARY_DELTA = 15,
SIM_KIN_TRIPOD = 16,
SIM_KIN_GENHEX = 17,
SIM_KIN_PENTAPOD = 18,
};
/* LinuxCNC kinematics mode semantics. Zero means the module default. */
enum sim_kinematics_mode {
SIM_KIN_MODE_DEFAULT = 0,
SIM_KIN_MODE_IDENTITY = 1,
SIM_KIN_MODE_FORWARD_ONLY = 2,
SIM_KIN_MODE_INVERSE_ONLY = 3,
SIM_KIN_MODE_BOTH = 4,
};
/* Delta mechanisms expose the two analytic roots through the flags fields.
* The default (zero) is LinuxCNC's lower/platform root; bit zero selects the
* alternate root for callers that need an explicit branch comparison. */
#define SIM_KIN_DELTA_ALTERNATE_ROOT UINT64_C(1)
/* Tripod forward flags select the below-base solution when set. */
#define SIM_KIN_TRIPOD_BELOW_ROOT UINT64_C(1)
/* Genhex forward flags select the alternate (below-platform) initial seed. */
#define SIM_KIN_GENHEX_ALTERNATE_SEED UINT64_C(2)
#define SIM_KIN_MAX_JOINTS 9u
#define SIM_KIN_GENHEX_STRUTS 6u
#define SIM_KIN_PENTA_STRUTS 5u
typedef struct sim_kinematics_config {
int type;
int mode;
uint32_t joint_count;
/* Total configured joints and the prefix owned by kinematics. */
uint32_t kinematics_joint_count;
uint32_t extra_joint_count;
char coordinates[16];
double x_rot_point;
double y_rot_point;
double z_rot_point;
double x_offset;
double y_offset;
double z_offset;
double tool_offset;
double pivot_length;
double scara_d1;
double scara_d2;
double scara_d3;
double scara_d4;
double scara_d5;
double scara_d6;
double puma_a2;
double puma_a3;
double puma_d3;
double puma_d4;
double puma_d6;
uint32_t dh_joint_count;
double dh_a[SIM_KIN_MAX_JOINTS];
double dh_alpha[SIM_KIN_MAX_JOINTS];
double dh_d[SIM_KIN_MAX_JOINTS];
double delta_radius;
double delta_rod_length;
double rotary_platform_radius;
double rotary_thigh_length;
double rotary_shin_length;
double rotary_foot_radius;
double tripod_bx;
double tripod_cx;
double tripod_cy;
double genhex_base[SIM_KIN_GENHEX_STRUTS][3];
double genhex_platform[SIM_KIN_GENHEX_STRUTS][3];
double genhex_base_axis[SIM_KIN_GENHEX_STRUTS][3];
double genhex_platform_axis[SIM_KIN_GENHEX_STRUTS][3];
double genhex_screw_lead;
double genhex_tool_offset;
double genhex_spindle_offset;
double genhex_max_error;
double genhex_convergence_tolerance;
uint32_t genhex_iteration_limit;
double genhex_initial_pose[6];
double penta_base[SIM_KIN_PENTA_STRUTS][3];
double penta_effector_radius[SIM_KIN_PENTA_STRUTS];
double penta_effector_z[SIM_KIN_PENTA_STRUTS];
double penta_tool_offset;
double penta_max_error;
double penta_convergence_tolerance;
uint32_t penta_iteration_limit;
double penta_initial_pose[5];
uint32_t max_iterations;
double convergence_tolerance;
uint8_t limit_enabled[SIM_KIN_MAX_JOINTS];
double joint_min[SIM_KIN_MAX_JOINTS];
double joint_max[SIM_KIN_MAX_JOINTS];
} sim_kinematics_config;
typedef struct sim_kinematics {
sim_kinematics_config config;
int axis_joint[SIM_KIN_MAX_JOINTS];
uint32_t configured;
uint64_t forward_flags;
uint64_t inverse_flags;
sim_posemath_pose last_pose;
double last_residual;
uint32_t last_iterations;
uint32_t branch_id;
int32_t revolution_count;
uint32_t previous_quadrant;
} sim_kinematics;
typedef struct sim_kinematics_result_info {
uint64_t forward_flags;
uint64_t inverse_flags;
double residual;
uint32_t iterations;
uint32_t branch_id;
} sim_kinematics_result_info;
int sim_kinematics_init(sim_kinematics *kinematics, const sim_kinematics_config *config);
int sim_kinematics_forward(const sim_kinematics *kinematics, const double *joints,
uint32_t joint_count, sim_posemath_pose *pose);
int sim_kinematics_forward_ex(const sim_kinematics *kinematics, const double *joints,
uint32_t joint_count, uint64_t inverse_flags,
sim_posemath_pose *pose, sim_kinematics_result_info *info);
int sim_kinematics_inverse(sim_kinematics *kinematics, const sim_posemath_pose *pose,
double *joints, uint32_t joint_count);
int sim_kinematics_inverse_ex(sim_kinematics *kinematics, const sim_posemath_pose *pose,
uint32_t joint_count, uint64_t forward_flags,
double *joints, sim_kinematics_result_info *info);
int sim_kinematics_home(const sim_kinematics *kinematics, double *joints, uint32_t joint_count,
sim_posemath_pose *pose);
int sim_kinematics_validate_pose(const sim_posemath_pose *pose);
uint64_t sim_kinematics_state_hash(const sim_kinematics *kinematics,
const double *joints, uint32_t joint_count,
const sim_posemath_pose *pose);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,66 @@
#ifndef LINUXCNC_WEB_WASM_SIM_MOTION_QUEUE_H
#define LINUXCNC_WEB_WASM_SIM_MOTION_QUEUE_H
#include "sim_posemath.h"
#ifdef __cplusplus
extern "C" {
#endif
enum sim_motion_result {
SIM_MOTION_OK = 0,
SIM_MOTION_ERR_ARGUMENT = -600,
SIM_MOTION_ERR_NONFINITE = -601,
SIM_MOTION_ERR_RANGE = -602,
SIM_MOTION_ERR_GEOMETRY = -603,
SIM_MOTION_ERR_MOTION_TYPE = -604,
SIM_MOTION_ERR_STATE = -605,
SIM_MOTION_ERR_CAPACITY = -606,
SIM_MOTION_ERR_BUFFER = -607,
};
#define SIM_MOTION_QUEUE_CAPACITY 64u
typedef struct sim_motion_command {
sim_posemath_segment segment;
uint32_t id;
uint32_t tag;
} sim_motion_command;
typedef struct sim_motion_state {
sim_posemath_pose position;
double velocity;
double progress;
uint32_t tick;
uint32_t queue_depth;
uint32_t active_id;
uint32_t active_tag;
int motion_type;
int status;
} sim_motion_state;
typedef struct sim_motion_queue {
sim_motion_command commands[SIM_MOTION_QUEUE_CAPACITY];
uint32_t head;
uint32_t count;
uint32_t next_id;
uint32_t tick;
double cycle_time;
double velocity_limit;
sim_motion_state state;
} sim_motion_queue;
int sim_motion_init(sim_motion_queue *queue, double cycle_time, double velocity_limit);
int sim_motion_reset(sim_motion_queue *queue);
int sim_motion_enqueue(sim_motion_queue *queue, const sim_motion_command *command, uint32_t *id_out);
int sim_motion_step(sim_motion_queue *queue);
int sim_motion_read_state(const sim_motion_queue *queue, sim_motion_state *state);
uint32_t sim_motion_queue_depth(const sim_motion_queue *queue);
uint64_t sim_motion_state_hash(const sim_motion_queue *queue);
uint32_t sim_motion_canonical(const sim_motion_queue *queue, char *output, uint32_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -5,12 +5,25 @@
extern "C" { extern "C" {
#endif #endif
#include <stdint.h>
enum sim_posemath_result { enum sim_posemath_result {
SIM_POSEMATH_OK = 0, SIM_POSEMATH_OK = 0,
SIM_POSEMATH_ERR_ARGUMENT = -400, SIM_POSEMATH_ERR_ARGUMENT = -400,
SIM_POSEMATH_ERR_NONFINITE = -401, SIM_POSEMATH_ERR_NONFINITE = -401,
SIM_POSEMATH_ERR_DEGENERATE = -402, SIM_POSEMATH_ERR_DEGENERATE = -402,
SIM_POSEMATH_ERR_RANGE = -403, SIM_POSEMATH_ERR_RANGE = -403,
SIM_POSEMATH_ERR_MOTION_TYPE = -404,
SIM_POSEMATH_ERR_STATE = -405,
SIM_POSEMATH_ERR_CAPACITY = -406,
};
/* Values match LinuxCNC's tc_motion_type_t. */
enum sim_posemath_motion_type {
SIM_POSEMATH_LINEAR = 1,
SIM_POSEMATH_CIRCULAR = 2,
SIM_POSEMATH_RIGID_TAP = 3,
SIM_POSEMATH_SPHERICAL = 4,
}; };
typedef struct sim_posemath_cartesian { typedef struct sim_posemath_cartesian {
@@ -19,11 +32,77 @@ typedef struct sim_posemath_cartesian {
double z; double z;
} sim_posemath_cartesian; } sim_posemath_cartesian;
typedef struct sim_posemath_pose {
sim_posemath_cartesian tran;
double a;
double b;
double c;
double u;
double v;
double w;
} sim_posemath_pose;
typedef struct sim_posemath_segment {
sim_posemath_pose start;
sim_posemath_pose end;
sim_posemath_cartesian center;
sim_posemath_cartesian normal;
double velocity;
double acceleration;
int motion_type;
int turn;
} sim_posemath_segment;
typedef struct sim_tp_state {
sim_posemath_pose position;
double velocity;
double progress;
uint32_t tick;
uint32_t queue_depth;
uint32_t active_id;
int motion_type;
int status;
} sim_tp_state;
#define SIM_TP_QUEUE_CAPACITY 32u
typedef struct sim_tp {
sim_posemath_segment queue[SIM_TP_QUEUE_CAPACITY];
uint32_t ids[SIM_TP_QUEUE_CAPACITY];
uint32_t head;
uint32_t count;
uint32_t next_id;
uint32_t active_id;
uint32_t tick;
double cycle_time;
double velocity_limit;
double acceleration_limit;
double progress;
double current_velocity;
sim_posemath_pose position;
int motion_type;
int status;
} sim_tp;
int sim_posemath_normalize(const sim_posemath_cartesian *input, sim_posemath_cartesian *output); int sim_posemath_normalize(const sim_posemath_cartesian *input, sim_posemath_cartesian *output);
int sim_posemath_dot(const sim_posemath_cartesian *left, const sim_posemath_cartesian *right, double *output); int sim_posemath_dot(const sim_posemath_cartesian *left, const sim_posemath_cartesian *right, double *output);
int sim_posemath_line_point(const sim_posemath_cartesian *start, const sim_posemath_cartesian *end, int sim_posemath_line_point(const sim_posemath_cartesian *start, const sim_posemath_cartesian *end,
double fraction, sim_posemath_cartesian *output); double fraction, sim_posemath_cartesian *output);
int sim_posemath_is_finite(const sim_posemath_cartesian *value); int sim_posemath_is_finite(const sim_posemath_cartesian *value);
int sim_posemath_pose_is_finite(const sim_posemath_pose *value);
int sim_posemath_pose_line_point(const sim_posemath_pose *start, const sim_posemath_pose *end,
double fraction, sim_posemath_pose *output);
int sim_posemath_segment_validate(const sim_posemath_segment *segment);
int sim_posemath_segment_point(const sim_posemath_segment *segment, double fraction,
sim_posemath_pose *output);
uint64_t sim_posemath_hash_state(const sim_tp_state *state);
int sim_tp_init(sim_tp *tp, double cycle_time, double velocity_limit, double acceleration_limit);
int sim_tp_reset(sim_tp *tp);
int sim_tp_enqueue(sim_tp *tp, const sim_posemath_segment *segment, uint32_t *id_out);
int sim_tp_step(sim_tp *tp);
int sim_tp_read_state(const sim_tp *tp, sim_tp_state *state);
uint64_t sim_tp_state_hash(const sim_tp *tp);
#ifdef __cplusplus #ifdef __cplusplus
} }

Some files were not shown because too many files have changed in this diff Show More