Implement WebAssembly runtime and kinematics coverage
This commit is contained in:
13
README.md
13
README.md
@@ -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` 策略。镜像只改变传输位置,不能改变版本、文件字节或校验记录。
|
||||
|
||||
@@ -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",
|
||||
|
||||
134
coverage/corexy-hal.report.json
Normal file
134
coverage/corexy-hal.report.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
436
coverage/coupled-kinematics.report.json
Normal file
436
coverage/coupled-kinematics.report.json
Normal 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"
|
||||
}
|
||||
361
coverage/delta-kinematics.report.json
Normal file
361
coverage/delta-kinematics.report.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
105
coverage/fiveaxis-kinematics.report.json
Normal file
105
coverage/fiveaxis-kinematics.report.json
Normal 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"
|
||||
}
|
||||
333
coverage/genhex-kinematics.report.json
Normal file
333
coverage/genhex-kinematics.report.json
Normal 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
|
||||
}
|
||||
}
|
||||
132
coverage/genser-kinematics.report.json
Normal file
132
coverage/genser-kinematics.report.json
Normal 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"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
361
coverage/interpreter-runtime.report.json
Normal file
361
coverage/interpreter-runtime.report.json
Normal 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"
|
||||
]
|
||||
}
|
||||
231
coverage/kinematics-abi-contract.json
Normal file
231
coverage/kinematics-abi-contract.json
Normal 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;"
|
||||
]
|
||||
}
|
||||
}
|
||||
27401
coverage/kinematics-config-graph.report.json
Normal file
27401
coverage/kinematics-config-graph.report.json
Normal file
File diff suppressed because it is too large
Load Diff
53
coverage/kinematics-golden-schema.json
Normal file
53
coverage/kinematics-golden-schema.json
Normal 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"
|
||||
}
|
||||
92
coverage/kinematics-mapping-contract.json
Normal file
92
coverage/kinematics-mapping-contract.json
Normal 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"
|
||||
}
|
||||
74
coverage/kinematics-numeric-policy.json
Normal file
74
coverage/kinematics-numeric-policy.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
11
coverage/kinematics-oracle-samples.jsonl
Normal file
11
coverage/kinematics-oracle-samples.jsonl
Normal file
File diff suppressed because one or more lines are too long
599
coverage/kinematics-oracle.report.json
Normal file
599
coverage/kinematics-oracle.report.json
Normal 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"
|
||||
}
|
||||
286
coverage/kinematics-runtime.report.json
Normal file
286
coverage/kinematics-runtime.report.json
Normal 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"
|
||||
]
|
||||
}
|
||||
5649
coverage/kinematics-source-closure.report.json
Normal file
5649
coverage/kinematics-source-closure.report.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
370
coverage/motion-runtime.report.json
Normal file
370
coverage/motion-runtime.report.json
Normal 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"
|
||||
}
|
||||
248
coverage/pentapod-kinematics.report.json
Normal file
248
coverage/pentapod-kinematics.report.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
229
coverage/robot-kinematics.report.json
Normal file
229
coverage/robot-kinematics.report.json
Normal 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"
|
||||
}
|
||||
@@ -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"}
|
||||
|
||||
197
coverage/tripod-kinematics.report.json
Normal file
197
coverage/tripod-kinematics.report.json
Normal 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
|
||||
}
|
||||
}
|
||||
307
coverage/trivkins-integration.report.json
Normal file
307
coverage/trivkins-integration.report.json
Normal 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"
|
||||
}
|
||||
}
|
||||
153
coverage/trt-kinematics.report.json
Normal file
153
coverage/trt-kinematics.report.json
Normal 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"
|
||||
}
|
||||
@@ -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。
|
||||
|
||||
本规范的目标不是把所有信息压进一个更大的包,而是让每个任务只携带完成当前结果所需的最小、可重建、可审计上下文。
|
||||
|
||||
@@ -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 里程碑门禁
|
||||
|
||||
|
||||
258
docs/linuxcnc-web-wasm-full-port-implementation.md
Normal file
258
docs/linuxcnc-web-wasm-full-port-implementation.md
Normal 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 功能标为完成。
|
||||
@@ -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、全量配置和浏览器回归。
|
||||
|
||||
@@ -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 或回退 |
|
||||
|
||||
|
||||
304
docs/linuxcnc-web-wasm-kinematics-migration-plan.md
Normal file
304
docs/linuxcnc-web-wasm-kinematics-migration-plan.md
Normal 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 发布的必要条件。
|
||||
@@ -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` |
|
||||
|
||||
@@ -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/**`、换刀测试 |
|
||||
|
||||
@@ -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
46
docs/task-history.md
Normal 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 默认不进入模型上下文。
|
||||
- 新交接只替换当前节点摘要;旧摘要保留在本索引或版本历史中,不能累积到指针文件。
|
||||
385
nextTask.md
385
nextTask.md
@@ -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,不跳到下一个节点。
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
112
scripts/generate_corexy_hal_report.py
Normal file
112
scripts/generate_corexy_hal_report.py
Normal 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())
|
||||
111
scripts/generate_coupled_kinematics_report.py
Normal file
111
scripts/generate_coupled_kinematics_report.py
Normal 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())
|
||||
112
scripts/generate_delta_kinematics_report.py
Normal file
112
scripts/generate_delta_kinematics_report.py
Normal 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())
|
||||
101
scripts/generate_fiveaxis_report.py
Normal file
101
scripts/generate_fiveaxis_report.py
Normal 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())
|
||||
99
scripts/generate_genhex_kinematics_report.py
Normal file
99
scripts/generate_genhex_kinematics_report.py
Normal 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())
|
||||
78
scripts/generate_genser_report.py
Normal file
78
scripts/generate_genser_report.py
Normal 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())
|
||||
97
scripts/generate_interpreter_report.py
Normal file
97
scripts/generate_interpreter_report.py
Normal 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())
|
||||
77
scripts/generate_kinematics_abi_contract.py
Normal file
77
scripts/generate_kinematics_abi_contract.py
Normal 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())
|
||||
272
scripts/generate_kinematics_closure_report.py
Normal file
272
scripts/generate_kinematics_closure_report.py
Normal 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())
|
||||
236
scripts/generate_kinematics_config_graph.py
Normal file
236
scripts/generate_kinematics_config_graph.py
Normal 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())
|
||||
65
scripts/generate_kinematics_mapping_contract.py
Normal file
65
scripts/generate_kinematics_mapping_contract.py
Normal 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())
|
||||
57
scripts/generate_kinematics_numeric_policy.py
Normal file
57
scripts/generate_kinematics_numeric_policy.py
Normal 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())
|
||||
111
scripts/generate_kinematics_oracle_report.py
Normal file
111
scripts/generate_kinematics_oracle_report.py
Normal 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())
|
||||
108
scripts/generate_kinematics_runtime_report.py
Normal file
108
scripts/generate_kinematics_runtime_report.py
Normal 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())
|
||||
70
scripts/generate_motion_report.py
Normal file
70
scripts/generate_motion_report.py
Normal 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())
|
||||
73
scripts/generate_pentapod_kinematics_report.py
Normal file
73
scripts/generate_pentapod_kinematics_report.py
Normal 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())
|
||||
104
scripts/generate_posemath_report.py
Normal file
104
scripts/generate_posemath_report.py
Normal 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())
|
||||
94
scripts/generate_robot_kinematics_report.py
Normal file
94
scripts/generate_robot_kinematics_report.py
Normal 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())
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
94
scripts/generate_tripod_kinematics_report.py
Normal file
94
scripts/generate_tripod_kinematics_report.py
Normal 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())
|
||||
52
scripts/generate_trivkins_integration_report.py
Normal file
52
scripts/generate_trivkins_integration_report.py
Normal 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())
|
||||
96
scripts/generate_trt_report.py
Normal file
96
scripts/generate_trt_report.py
Normal 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
426
scripts/model_stream.py
Normal 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"]
|
||||
@@ -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)
|
||||
|
||||
36
scripts/test_corexy_hal_report.py
Normal file
36
scripts/test_corexy_hal_report.py
Normal 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()
|
||||
38
scripts/test_coupled_kinematics_report.py
Normal file
38
scripts/test_coupled_kinematics_report.py
Normal 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()
|
||||
38
scripts/test_delta_kinematics_report.py
Normal file
38
scripts/test_delta_kinematics_report.py
Normal 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()
|
||||
32
scripts/test_fiveaxis_report.py
Normal file
32
scripts/test_fiveaxis_report.py
Normal 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()
|
||||
29
scripts/test_generate_posemath_report.py
Normal file
29
scripts/test_generate_posemath_report.py
Normal 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()
|
||||
32
scripts/test_genhex_kinematics_report.py
Normal file
32
scripts/test_genhex_kinematics_report.py
Normal 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()
|
||||
28
scripts/test_genser_report.py
Normal file
28
scripts/test_genser_report.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
37
scripts/test_interpreter_runtime.py
Normal file
37
scripts/test_interpreter_runtime.py
Normal 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()
|
||||
40
scripts/test_kinematics_abi_contract.py
Normal file
40
scripts/test_kinematics_abi_contract.py
Normal 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()
|
||||
60
scripts/test_kinematics_closure_report.py
Normal file
60
scripts/test_kinematics_closure_report.py
Normal 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()
|
||||
80
scripts/test_kinematics_config_graph.py
Normal file
80
scripts/test_kinematics_config_graph.py
Normal 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()
|
||||
19
scripts/test_kinematics_mapping_contract.py
Normal file
19
scripts/test_kinematics_mapping_contract.py
Normal 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()
|
||||
18
scripts/test_kinematics_numeric_policy.py
Normal file
18
scripts/test_kinematics_numeric_policy.py
Normal 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()
|
||||
44
scripts/test_kinematics_oracle_report.py
Normal file
44
scripts/test_kinematics_oracle_report.py
Normal 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()
|
||||
51
scripts/test_kinematics_runtime.py
Normal file
51
scripts/test_kinematics_runtime.py
Normal 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()
|
||||
154
scripts/test_model_stream.py
Normal file
154
scripts/test_model_stream.py
Normal 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()
|
||||
37
scripts/test_motion_runtime.py
Normal file
37
scripts/test_motion_runtime.py
Normal 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()
|
||||
27
scripts/test_pentapod_kinematics_report.py
Normal file
27
scripts/test_pentapod_kinematics_report.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
31
scripts/test_robot_kinematics_report.py
Normal file
31
scripts/test_robot_kinematics_report.py
Normal 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()
|
||||
33
scripts/test_tripod_kinematics_report.py
Normal file
33
scripts/test_tripod_kinematics_report.py
Normal 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()
|
||||
18
scripts/test_trivkins_integration_report.py
Normal file
18
scripts/test_trivkins_integration_report.py
Normal 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()
|
||||
31
scripts/test_trt_report.py
Normal file
31
scripts/test_trt_report.py
Normal 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()
|
||||
207
scripts/test_validate_model_budget.py
Normal file
207
scripts/test_validate_model_budget.py
Normal 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()
|
||||
63
scripts/test_validate_task_pointer.py
Normal file
63
scripts/test_validate_task_pointer.py
Normal 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()
|
||||
@@ -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")
|
||||
|
||||
501
scripts/validate_model_budget.py
Normal file
501
scripts/validate_model_budget.py
Normal 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())
|
||||
89
scripts/validate_task_pointer.py
Normal file
89
scripts/validate_task_pointer.py
Normal 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())
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
74
wasm-core/include/sim_interpreter.h
Normal file
74
wasm-core/include/sim_interpreter.h
Normal 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
|
||||
174
wasm-core/include/sim_kinematics.h
Normal file
174
wasm-core/include/sim_kinematics.h
Normal 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
|
||||
66
wasm-core/include/sim_motion_queue.h
Normal file
66
wasm-core/include/sim_motion_queue.h
Normal 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
|
||||
@@ -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
Reference in New Issue
Block a user