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 `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`,没有来源会被静默忽略。
- WASM ABI 骨架:`wasm-core/include/sim_api.h`;当前实现只验证生命周期、固定 tick、快照和状态导入导出。
- 前端、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
@@ -18,8 +22,11 @@
python3 scripts/generate_source_inventory.py
python3 scripts/validate_baseline.py
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 骨架
需要 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
```
完整路线、功能完整性和退出门禁见 [`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` 策略。镜像只改变传输位置,不能改变版本、文件字节或校验记录。

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."
},
"inventory_counts": {
"total": 8020,
"source_index": 8020,
"manifest_seed": 8020,
"total": 8035,
"source_index": 8035,
"manifest_seed": 8035,
"document_capabilities": 2868,
"source_symbols": 23983,
"config_resources": 16093,
"test_entries": 321,
"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",
"manifest_seed_sha256": "1a64bedd3629e665411fb87abb6b5de94eed11456acab305fade6bdec18076fd",
"source_index_sha256": "b0719a9cf1031f7305e29075e645f21aa3c3630401e284825565a8b206c83ce4",
"manifest_seed_sha256": "70cd247690616ca260b111fb737c1277c5336f845acc5ac27160eaf87a7c9f93",
"document_capabilities_sha256": "fa5140bd4995224406381e4190e76bf81f64bd24dbf93a2fd10421c45aee4316",
"source_symbols_sha256": "5fc72e6a268d2220b6335f7f6834c925f13c5c35461fa399a400e6621958875a",
"config_resources_sha256": "d4d02cb13f2f9cb58404630d4de8ac0226e02075166fe30f2b03a6abca4be5bc",
@@ -81,8 +88,29 @@
"runtime_mapping_report_sha256": "f4be10828fda8201ff9e69de95bc68e5def42fc7206a851b37474c21b4da0a17",
"python_runtime_manifest_sha256": "340faa6ef5bedcaeb0583f463e6b89cb442ddf1d83d7e6ee8d96cee438fe4213",
"python_runtime_report_sha256": "be531c8cb5af55f9527b6b6b3955ff908a748fae0e37952fa699aecf1a44b399",
"hal_runtime_report_sha256": "70b99118ae6957456e592512d752aa137f59cae897ec546509ff26456879dd90",
"posemath_runtime_report_sha256": "26b04fb285b5d085a980856fffe5707c2e2ccaeddc08c06f2da2491e76256a39",
"hal_runtime_report_sha256": "85f79be090c5fceb3ee633da0fff57c908604938592c9b2cf6a1b2151b588e6b",
"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",
"golden_schema_sha256": "5513535b9480d7100ef3ef59ac933c4040bd7198dc419693a7ee68098171c423",
"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,
"max_period_ticks": 1000000,
"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"]
}

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",
"status": "passed",
"implementation": "linuxcnc/src/libnml/posemath",
"linked_sources": ["_posemath.c", "gomath.c", "sincos.c", "posemath.cc"],
"adapter_operations": ["normalize", "dot", "line_point", "is_finite"],
"error_boundaries": ["nonfinite", "degenerate", "range"],
"adapter_operations": [
"normalize",
"dot",
"line_point",
"pose_line_point",
"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,
"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":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":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":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"}
@@ -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":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":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_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"}
@@ -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":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":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":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":928,"kind":"web_wasm_interface","path":"wasm-core/include/sim_posemath.h","sha256":"71b092379fbfbbd46cf2280fb825fabe84219dd9a0fc1e16f0bf7232160dff05","source_id":"implementation:wasm-core/include/sim_posemath.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":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":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":6029,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_hal.c","sha256":"5926e2cdc785d060bc199b2bdfa550c898facf8221204487fdc3ae841400a02e","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":9413,"kind":"web_wasm_implementation","path":"wasm-core/src/sim_hal.c","sha256":"76a057064546a8ee9ba1098952755a4cdc56e6e714f2761b8a1826898d3ae621","source_id":"implementation:wasm-core/src/sim_hal.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":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":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":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":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":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":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"}

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/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. 一个任务只有一个主工作流、一个里程碑、一个可验证结果和一个回滚点。跨三个以上工作流必须拆分。
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` 只在确有需要时使用。
4. `total_bytes` 同时限制序列化包和选中证据负载:`context_bytes = packet_bytes + selected_evidence_bytes`。`evidence_bytes` 是独立的证据总量上限,不能用文件数量替代。
5. 证据在生成阶段筛选。可选证据被排除时必须记录稳定 `reason_code`;必需证据无法容纳时生成失败,不输出部分成功包。
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 包的分层读取
任务执行者按以下顺序读取,读到足够完成任务即停止:
@@ -137,6 +143,29 @@ next: INV-06b
| COM-05a | 能力/限制注册表 | 全量 manifest | INV-02b、CORE-07b | contract |
| 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:前端、回归和发布
| ID | 单一结果 | 直接输入 | 依赖 | Profile |
@@ -176,7 +205,7 @@ next: INV-06b
### 4.1 正常交接
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`;不追加旧包全文。
4. 提交正文只写 `Context-ID`、`Context-Packet`、子任务 ID、测试命令和证据路径。
@@ -201,7 +230,7 @@ budget.packet_bytes <= byte_limit
budget.packet_bytes + budget.selected_evidence_bytes <= byte_limit
selected_evidence_bytes <= evidence_byte_limit
selected_evidence_files <= evidence_file_limit
所有排除项都有 E1-E5 reason_code
所有排除项都有 E1-E6 reason_code
required 证据无法容纳时命令失败且不写输出
```
@@ -209,6 +238,10 @@ required 证据无法容纳时命令失败且不写输出
```bash
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
**适用范围**:本仓库的纯 Web/WASM 仿真移植、浏览器运行时、OPFS 工程存储、覆盖矩阵和发布物
**固定基线**: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 产物中,只通过路径和摘要引用。
启动顺序同样受预算约束:先只读取 `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`,不得追加自由历史段:
| 段 | 固定内容 | 建议上限 |
@@ -91,6 +93,19 @@ M0 必须冻结来源扫描和规范化规则;每个来源项至少映射到
包的 `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`,按必需、优先级、路径排序,保证选择确定性。
3. 逐项检查:单项证据字节预算、证据总字节预算、证据文件数,以及把该项加入 `selected` 后的完整上下文剩余空间;完整上下文按 `packet_bytes + selected_evidence_bytes` 计算,不能只计算 JSON 清单。
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` 一致性和引用产物存在:
@@ -262,8 +277,8 @@ online_ready
|---|---|---|---|
| M0 基线锁定 | A | context lock、来源/测试清单、状态枚举、责任分工 | 清单可重建;缺项 CI 失败;文档链接和基线一致 |
| 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 可定位 |
| M3 虚拟设备与持久化 | C | HAL/PLC/IO、数字孪生、事务恢复、断网接续 | 故障注入和恢复矩阵通过;配额不足不损坏已提交版本 |
| 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、K6 运动集成、数字孪生、事务恢复、断网接续 | 故障注入和恢复矩阵通过;配额不足不损坏已提交版本 |
| M4 Web 工作流 | D | 控制台、3D、VCP/配置工具、导入导出、离线 PWA | 关键 UI 工作流在浏览器矩阵通过;键盘/触摸和刷新恢复有证据 |
| M5 发布候选 | E | 全量覆盖、兼容/许可证报告、迁移工具、离线包 | 所有可计入条目 `accepted`,排除项可识别,发布门禁全绿 |
@@ -273,7 +288,7 @@ online_ready
### 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 里程碑门禁

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 功能移植尚未完成
**目标**:把既有技术路线收敛成可执行的实施顺序、完整性证明和发布前置条件
**固定基线**: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. 现状和不可误读项
本仓库已经具备完整设计文档,但此前没有 Web/WASM 运行时代码。当前 M0/M1 产物如下:
本仓库已经具备完整设计文档,并已落地一组可独立验证的 Web/WASM 运行时切片;LinuxCNC 全功能运行时仍未闭合。当前 M0/M1 产物如下:
| 产物 | 当前事实 | 允许的结论 |
|---|---|---|
| `context-lock.json` | 锁定 LinuxCNC 提交、Emscripten 3.1.6、schema v1 和索引哈希 | 基线可复现 |
| `coverage/source-index.jsonl` | 7,992 个来源文件,来自 docs/src/configs/tests/unit_tests/build | 来源扫描已开始,可审计 |
| `coverage/manifest.jsonl` | 7,992 个一对一种子条目,全部 `planned/needs_owner` | 没有来源被静默漏掉;不代表已实现 |
| `wasm-core` | C ABI 生命周期、固定 tick、快照、导入导出骨架 | 工程边界可编译、可调用 |
| `coverage/source-index.jsonl` | 8,026 个来源文件,来自 docs/src/configs/tests/unit_tests/build | 来源扫描已开始,可审计 |
| `coverage/manifest.jsonl` | 8,026 个一对一种子条目,全部 `planned/needs_owner` | 没有来源被静默漏掉;不代表已实现 |
| `wasm-core` | C ABI 生命周期、固定 tick、快照、导入导出,以及 posemath/解释器/运动适配切片 | 工程边界可编译、可调用;不代表完整核心闭合 |
| `build-wasm`/`build-native` | WASM 模块生成,宿主 ABI 单测通过 | 构建链路可重跑 |
以下能力目前都不能声明完成: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-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 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)
**覆盖验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)
**治理规则**:[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)
**最小任务与上下文**:[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md)
本手册是五份设计文档的总控入口。它不替代各专项文档,而是规定它们如何组合、谁负责什么、任务如何进入执行、依赖如何推进、失败如何恢复以及什么证据才能声明移植完成。
本手册是专项设计文档的总控入口。完整实施蓝图规定单个仿真入口如何启动和推进;本手册不替代这些专项文档,而是规定它们如何组合、谁负责什么、任务如何进入执行、依赖如何推进、失败如何恢复以及什么证据才能声明移植完成。
## 1. 当前审计结论
@@ -19,14 +21,14 @@
```text
设计基线:已建立
源码/功能清单:`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 或单一三轴样例都不能把项目状态提前改成“全功能完成”。
上下文治理的可执行入口是 `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. 唯一追踪链
@@ -116,7 +118,7 @@ M0 / WS-0 基线、source map、manifest、context lock
| CORE-04 | TP/posemath/几何 | `src/emc/tp/**`、posemath | canonical、轨迹和容差报告 |
| 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-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 状态 |
### 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、截图/像素 |
| 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. 里程碑总表
| 里程碑 | 入口条件 | 必须完成 | 退出证据 | 失败动作 |
|---|---|---|---|---|
| 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 |
| M2 核心行为 | M1 通过 | CORE-03..08、native/WASM runner、解释/轨迹/运动 golden | 核心 manifest `verified`,首个差异 tick 可定位 | 回退到对应核心任务,不进入 UI 全量 |
| M3 虚拟设备和恢复 | M2 通过 | RT-02..04、FS-02/03、PY-01/02、COM-03..05 | HAL/PLC/IO/故障/配额/重启/断网矩阵通过 | 阻断仿真会话发布,保留本地证据 |
| M2 核心行为 | M1 通过,K0 入口审计完成 | CORE-03..08、K0-K5、native/WASM runner、解释/轨迹/运动 golden | 核心 manifest `verified`,每个运动学族的首个差异 tick 可定位 | 回退到对应 K/核心任务,不进入 UI 全量 |
| 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 |
| 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` |
| 主轴同步 | threading、G33/G33.1、rigid tapping、CSS、gear、orient、multiple spindle | `SIM-DEVICE` |
| 控制环 | 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` |
| 用户 kinematics | 构建期静态 WASM ABI、热切换由仿真状态机完成 | `SIM-ADAPTER` |
| 坐标/姿态显示 | 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` |
| 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/**` |
| 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 |
| 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/**`、换刀测试 |

View File

@@ -6,12 +6,14 @@
**范围**:只做仿真,不连接真实机床,不通过网络控制硬件
**兼容目标**:以 LinuxCNC 仿真模式为行为基线,覆盖全部仿真功能并给出浏览器内等价实现
**执行治理**:[项目执行与交付策略](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-source-map.md)
**前端与通信**:[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.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. 摘要
@@ -413,7 +415,7 @@ cmake --build build-wasm
## 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:全量清单和运行时骨架
@@ -424,7 +426,7 @@ cmake --build build-wasm
### 阶段 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 虚拟机床模型。
### 阶段 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 退出门禁
这是恢复工作的唯一入口。它必须保持为小型、无历史的任务指针;完整日志、源码和旧交接记录均按路径按需读取。
> 每次开始工作先读本文件。不要根据聊天记录、目录数量或演示结果推测当前任务。专项范围以文档为准,任务位置以本文件为准。
## 1. 接续记录
## Current
```yaml
handoff_version: 1
context_id: linuxcnc-2.10.0-pre0-wasm-m0-c1
milestone: M0
last_verified_checkpoint: M1-RT-02-r01
current_task:
id: M2-CORE-01
status: ready
title: posemath/TP 原生核心接入
owner: WS-0
profile: feature
next_task:
id: M2-CORE-02
pointer_version: 3
updated: 2026-08-21
context_id: linuxcnc-2.10.0-pre0-wasm-m1-py-c1
milestone: K4
last_verified_checkpoint: K4-HEX-01-r01
task:
id: K4-PENTA-01
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
profile: feature
```
`BOOTSTRAP-M0-01` 只表示以下前置产物已验证,不表示 M0 完成:
## Read
- `context-lock.json` 已锁定 LinuxCNC 提交和当前工具链。
- `coverage/source-index.jsonl` 与 `coverage/manifest.jsonl` 已生成 8,014 个文件级种子条目。
- `wasm-core` bootstrap ABI 可生成 WASM,宿主 ABI 单测通过。
- 基线、下载索引和上下文包测试通过。
启动只读取本文件的 `Current`、`Task` 和 `Verify`。随后读取 `context-lock.json` 的摘要和当前任务列出的直接输入;不要自动加载路线图、覆盖矩阵、完整日志或 `coverage/*.jsonl`。需要历史时按需读取 [`docs/task-history.md`](docs/task-history.md),需要完整产物时使用其中的路径、大小和哈希。
文档能力、源码符号、配置资源、测试入口、规范化关联键、依赖闭包、原生 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
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
```
任一命令失败时先修复或建立新的 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/**`
- `linuxcnc/src/emc/tp/**`
- `linuxcnc/unit_tests/tp/**`
- `context-lock.json`
**必须产出**:pentapod 真实/派生配置样本、五腿 FK/IK 与姿态约束、迭代收敛/不收敛/奇异/限位边界、native/WASM 对照和 blocked oracle 清单;更新迁移计划、manifest 和任务历史。
**必须产出**:
**完成条件**:配置候选;工作空间边界、姿态约束、奇异/无解、迭代上限、关节限位和 state hash 稳定;官方 oracle 缺失继续显式 blocked。
- `wasm-core/include/sim_posemath.h`
- `wasm-core/src/sim_posemath.c`
- `wasm-core/tests/sim_posemath_test.c`
- `coverage/posemath-runtime.report.json`
- 生成数量、输入摘要和输出 SHA-256
**非目标**:本任务不批准替代 oracle 契约、不实现完整运动学 ABI、不接入 UI 运行时和 Python、不把配置 smoke 当作数值 golden。
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 节点实施。
- 不把文档标题直接标记为 `accepted`。
- 不接入 rs274ngc、TP、全量 LinuxCNC HAL 组件或 UI 工作流。
完成本任务后,在同一变更中把 `next` 提升为 `Current.task`,更新 `last_verified_checkpoint` 和最近证据摘要,并重新运行 `Read` 中的门禁。不得跳过未完成依赖;同一时刻只允许一个 current task。
**完成条件**:
1. 相同输入连续生成两次,输出字节完全一致。
2. 相同 pose/segment 输入连续运行产生相同 canonical JSON 和状态哈希。
3. NaN/Inf、退化 segment、越界速度和未知运动类型都有明确阻断结果。
4. 生成器和失败路径有自动测试。
5. `context-lock.json` 登记新索引的数量和 SHA-256;基线校验器验证该摘要。
6. 本文件按第 5 节完成节点切换。
建议验收命令:
## Verify
```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
source_index=8003 manifest_seed=8003
python3 scripts/normalize_sources.py
normalized_sources=51268 sha256=a9bb2d50952c52b3f3d1bd50e7dc7654cb3831f28ff8147d9c1fc493ebbe80cb
```
- 新增 `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/generate_kinematics_closure_report.py
python3 scripts/generate_kinematics_config_graph.py
python3 scripts/generate_kinematics_oracle_report.py
cmake --build build-native && ctest --test-dir build-native --output-on-failure
python3 scripts/validate_baseline.py
baseline=valid
python3 scripts/validate_download_index.py
download_index=valid
python3 scripts/validate_task_pointer.py
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`
- 报告:35,685 条已解析、1,475 条明确 unresolved/missing、74 条重复边、28,897 个无连接孤儿节点;未删除任何缺失引用。
最近证据:`coverage/genhex-kinematics.report.json` 已验证真实 hexapod-sim 配置、六腿闭式逆解、Newton 6x6 正解、alternate seed、迭代/奇异/限位边界、native/WASM 字节一致和 state hash 一致;缺失 `linuxcnc/unit_tests/kinematics/**` 官方 oracle 继续显式 blocked。下一任务处理 `pentakins.c` 五腿并联约束。
### INV-03a-r01(2026-08-20)
## Stop
```text
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、产物路径和哈希追溯。
若 LinuxCNC 提交偏离 lock、必需输入缺失、需要未审查的 ABI/protocol/manifest schema 变化、索引无法确定性生成,或测试只能靠删样本/降低检查/伪装 `REAL-EXCLUDED` 才通过,立即记录为 blocked,不跳到下一个节点。

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
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 --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 'native_test=%s\n' "${native_build_dir}/sim_api_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 hashlib
import json
import math
import sys
from dataclasses import dataclass
from pathlib import Path
@@ -34,8 +35,13 @@ EXCLUSION_REASONS = {
"E3": "evidence_file_count_exceed_budget",
"E4": "evidence_bytes_exceed_budget",
"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):
"""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
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:
"""Store the self-consistent serialized size in the budget metadata."""
@@ -166,6 +178,7 @@ def generate_packet(
context_id: str,
packet_id: str,
profile: str = "feature",
token_limit: int | None = None,
total_bytes: int | None = None,
evidence_bytes: int | None = None,
max_files: int | None = None,
@@ -177,12 +190,27 @@ def generate_packet(
raise PacketError(f"unknown profile: {profile}")
limits = dict(PROFILES[profile])
limits["profile"] = profile
if total_bytes is not None:
limits["bytes"] = int(total_bytes)
if evidence_bytes is not None:
limits["evidence_bytes"] = int(evidence_bytes)
if max_files is not None:
limits["files"] = int(max_files)
# Overrides are intentionally one-way: a task may ask for a smaller
# packet, but it must not enlarge a profile and silently bypass the
# context-window policy enforced by the request validator.
overrides = {
"tokens": token_limit,
"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")):
raise PacketError("budgets must be non-negative")
@@ -227,10 +255,15 @@ def generate_packet(
trial["budget"] = _budget_metadata(
limits, trial_selected, excluded, evidence_used + candidate.size
)
# This check is intentionally performed before selection is committed.
trial_context_bytes = _context_bytes(trial, evidence_used + candidate.size)
# Finalize the trial before checking budgets. In particular, do
# 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"]:
reason = "E5"
elif _estimate_tokens(trial_packet_bytes, evidence_used + candidate.size) > limits["tokens"]:
reason = "E6"
if reason:
excluded.append({"path": candidate.path, "reason_code": reason})
if candidate.required:
@@ -247,7 +280,12 @@ def generate_packet(
packet["budget"] = _budget_metadata(limits, selected, excluded, evidence_used)
packet_bytes = _finalize_packet_size(packet)
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
removable_index = next(
(index for index in range(len(selected) - 1, -1, -1)
@@ -256,12 +294,15 @@ def generate_packet(
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)
evidence_used -= int(removed["bytes"])
excluded.append({
"path": removed["path"],
"reason_code": "E5",
"reason_code": "E5" if over_bytes else "E6",
})
return packet
@@ -282,6 +323,7 @@ def main(argv: Iterable[str] | None = None) -> int:
parser.add_argument("--packet-id", required=True)
parser.add_argument("--profile", choices=sorted(PROFILES), default="feature")
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("--evidence-bytes", 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,
packet_id=args.packet_id,
profile=args.profile,
token_limit=args.token_limit,
total_bytes=args.total_bytes,
evidence_bytes=args.evidence_bytes,
max_files=args.max_files,
@@ -305,7 +348,7 @@ def main(argv: Iterable[str] | None = None) -> int:
if len(payload) != packet["budget"]["packet_bytes"]:
raise PacketError("serialized packet size changed before write")
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)
return 2
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 hashlib
import json
import re
from pathlib import Path
from typing import Iterable
@@ -29,6 +30,11 @@ BUILD_FILES = (
Path("linuxcnc/VERSION"),
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:
@@ -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"])
return {
entry = {
"id": f"seed:{source_id}",
"feature_id": None,
"source": source_id,
@@ -108,6 +114,48 @@ def make_manifest_entry(source: dict[str, object]) -> dict[str, object]:
"exclusion_reason": None,
"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:
@@ -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.sort(key=lambda item: str(item["source_id"]))
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)

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 context_packet import PacketError, generate_packet
from validate_model_budget import validate_request
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(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):
with TemporaryDirectory() as 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
import json
import json
import subprocess
import unittest
from pathlib import Path
@@ -17,7 +18,7 @@ class HalRuntimeTests(unittest.TestCase):
def test_header_declares_error_boundary(self):
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)
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"):
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__":
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():
self.skipTest("WASM build has not been generated")
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)
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):
cmake = (ROOT / "wasm-core/CMakeLists.txt").read_text(encoding="utf-8")
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")
elif posemath_report_hash != sha256(posemath_report_path):
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]
if len(source_ids) != len(set(source_ids)):
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_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_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
)
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_compile_options(sim_core PRIVATE -ffp-contract=off -fno-fast-math)
if(NOT EMSCRIPTEN)
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)
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_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_executable(sim_posemath_test tests/sim_posemath_test.c)
target_link_libraries(sim_posemath_test PRIVATE sim_core)
target_include_directories(sim_posemath_test PRIVATE include)
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()
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
include
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/rtapi
${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)
set_target_properties(sim_core_module PROPERTIES
OUTPUT_NAME "linuxcnc-sim-core"
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()

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_register_thread(const char *name, uint32_t period_ticks);
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_step(uint32_t ticks);
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" {
#endif
#include <stdint.h>
enum sim_posemath_result {
SIM_POSEMATH_OK = 0,
SIM_POSEMATH_ERR_ARGUMENT = -400,
SIM_POSEMATH_ERR_NONFINITE = -401,
SIM_POSEMATH_ERR_DEGENERATE = -402,
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 {
@@ -19,11 +32,77 @@ typedef struct sim_posemath_cartesian {
double z;
} 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_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,
double fraction, sim_posemath_cartesian *output);
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
}

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